diff --git a/editor/css/main.css b/editor/css/main.css index 8c71cc33cdffe7..d7d7a0de4624d9 100644 --- a/editor/css/main.css +++ b/editor/css/main.css @@ -1,864 +1,924 @@ :root { - color-scheme: light dark; + color-scheme: light dark; } [hidden] { - display: none !important; + display: none !important; } body { - font-family: Helvetica, Arial, sans-serif; - font-size: 14px; - margin: 0; - overflow: hidden; + font-family: Helvetica, Arial, sans-serif; + font-size: 14px; + margin: 0; + overflow: hidden; } hr { - border: 0; - border-top: 1px solid #ccc; + border: 0; + border-top: 1px solid #ccc; } button { - position: relative; + position: relative; } input { - vertical-align: middle; + vertical-align: middle; } - input[type="color"]::-webkit-color-swatch-wrapper { - padding: 0; - } - input[type="color"]::-webkit-color-swatch { - border: none; - } +input[type="color"]::-webkit-color-swatch-wrapper { + padding: 0; +} +input[type="color"]::-webkit-color-swatch { + border: none; +} textarea { - tab-size: 4; - white-space: pre; - word-wrap: normal; + tab-size: 4; + white-space: pre; + word-wrap: normal; } - textarea.success { - border-color: #8b8 !important; - } +textarea.success { + border-color: #8b8 !important; +} - textarea.fail { - border-color: #f00 !important; - background-color: rgba(255,0,0,0.05); - } +textarea.fail { + border-color: #f00 !important; + background-color: rgba(255, 0, 0, 0.05); +} -textarea, input { outline: none; } /* osx */ +textarea, +input { + outline: none; +} /* osx */ .Panel { - -moz-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; - /* No support for these yet */ - -o-user-select: none; - user-select: none; + /* No support for these yet */ + -o-user-select: none; + user-select: none; } .TabbedPanel { - -moz-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; - /* No support for these yet */ - -o-user-select: none; - user-select: none; - position: relative; - display: block; - width: 100%; - min-width: 335px; + /* No support for these yet */ + -o-user-select: none; + user-select: none; + position: relative; + display: block; + width: 100%; + min-width: 335px; } .TabbedPanel .Tabs { - position: relative; - z-index: 1; /** Above .Panels **/ - display: block; - width: 100%; - white-space: pre; - overflow: hidden; - overflow-x: auto; -} - - .TabbedPanel .Tabs::-webkit-scrollbar { - height: 5px; - background: #eee; - } - .TabbedPanel .Tabs::-webkit-scrollbar-thumb { - background: #08f3; - } - .TabbedPanel .Tabs:hover::-webkit-scrollbar-thumb { - background: #08f; - cursor: ew-resize; - } - - .TabbedPanel .Tabs .Tab { - padding: 10px 9px; - text-transform: uppercase; - } - - .TabbedPanel .Panels { - position: absolute; - top: 40px; - display: block; - width: 100%; - } + position: relative; + z-index: 1; /** Above .Panels **/ + display: block; + width: 100%; + white-space: pre; + overflow: hidden; + overflow-x: auto; +} + +.TabbedPanel .Tabs::-webkit-scrollbar { + height: 5px; + background: #eee; +} +.TabbedPanel .Tabs::-webkit-scrollbar-thumb { + background: #08f3; +} +.TabbedPanel .Tabs:hover::-webkit-scrollbar-thumb { + background: #08f; + cursor: ew-resize; +} + +.TabbedPanel .Tabs .Tab { + padding: 10px 9px; + text-transform: uppercase; +} + +.TabbedPanel .Panels { + position: absolute; + top: 40px; + display: block; + width: 100%; +} /* Listbox */ .Listbox { - color: #444; - background-color: #fff; - padding: 0; - width: 100%; - min-height: 180px; - font-size: 12px; - cursor: default; - overflow: auto; + color: #444; + background-color: #fff; + padding: 0; + width: 100%; + min-height: 180px; + font-size: 12px; + cursor: default; + overflow: auto; } .Listbox .ListboxItem { - padding: 6px; - color: #666; - white-space: nowrap; + padding: 6px; + color: #666; + white-space: nowrap; } .Listbox .ListboxItem.active { - background-color: rgba(0, 0, 0, 0.04); + background-color: rgba(0, 0, 0, 0.04); } /* CodeMirror */ .CodeMirror { - - position: absolute !important; - top: 37px; - width: 100% !important; - height: calc(100% - 37px) !important; - + position: absolute !important; + top: 37px; + width: 100% !important; + height: calc(100% - 37px) !important; } - .CodeMirror .errorLine { - - background: rgba(255,0,0,0.25); - - } - - .CodeMirror .esprima-error { - - color: #f00; - text-align: right; - padding: 0 20px; +.CodeMirror .errorLine { + background: rgba(255, 0, 0, 0.25); +} - } +.CodeMirror .esprima-error { + color: #f00; + text-align: right; + padding: 0 20px; +} /* outliner */ #outliner .opener { - display: inline-block; - width: 14px; - height: 14px; - margin: 0px 4px; - vertical-align: top; - text-align: center; + display: inline-block; + width: 14px; + height: 14px; + margin: 0px 4px; + vertical-align: top; + text-align: center; } - #outliner .opener.open:after { - content: '−'; - } +#outliner .opener.open:after { + content: "−"; +} - #outliner .opener.closed:after { - content: '+'; - } +#outliner .opener.closed:after { + content: "+"; +} #outliner .option { - - border: 1px solid transparent; - + border: 1px solid transparent; } #outliner .option.drag { - - border: 1px dashed #999; - + border: 1px dashed #999; } #outliner .option.dragTop { - - border-top: 1px dashed #999; - + border-top: 1px dashed #999; } #outliner .option.dragBottom { - - border-bottom: 1px dashed #999; - + border-bottom: 1px dashed #999; } #outliner .type { - display: inline-block; - width: 14px; - height: 14px; - color: #ddd; - text-align: center; + display: inline-block; + width: 14px; + height: 14px; + color: #ddd; + text-align: center; } #outliner .type:after { - content: '●'; + content: "●"; } /* */ #outliner .Scene { - color: #8888dd; + color: #8888dd; } #outliner .Camera { - color: #dd8888; + color: #dd8888; } #outliner .Light { - color: #dddd88; + color: #dddd88; } /* */ #outliner .Object3D { - color: #aaaaee; + color: #aaaaee; } #outliner .Mesh { - color: #8888ee; + color: #8888ee; } #outliner .Line { - color: #88ee88; + color: #88ee88; } #outliner .LineSegments { - color: #88ee88; + color: #88ee88; } #outliner .Points { - color: #ee8888; + color: #ee8888; } /* */ #outliner .Geometry { - color: #aaeeaa; + color: #aaeeaa; } #outliner .Material { - color: #eeaaee; + color: #eeaaee; } /* */ #outliner .Script:after { - content: '◎' + content: "◎"; } /* */ button { - color: #555; - background-color: #ddd; - border: 0px; - margin: 0px; /* GNOME Web */ - padding: 5px 8px; - font-size: 12px; - text-transform: uppercase; - cursor: pointer; - outline: none; + color: #555; + background-color: #ddd; + border: 0px; + margin: 0px; /* GNOME Web */ + padding: 5px 8px; + font-size: 12px; + text-transform: uppercase; + cursor: pointer; + outline: none; } - button:hover { - background-color: #fff; - } +button:hover { + background-color: #fff; +} - button.selected { - background-color: #fff; - } +button.selected { + background-color: #fff; +} -input, textarea { - border: 1px solid transparent; - color: #444; +input, +textarea { + border: 1px solid transparent; + color: #444; } input.Number { - color: #08f!important; - font-size: 12px; - border: 0px; - padding: 2px; + color: #08f !important; + font-size: 12px; + border: 0px; + padding: 2px; } select { - color: #666; - background-color: #ddd; - border: 0px; - text-transform: uppercase; - cursor: pointer; - outline: none; + color: #666; + background-color: #ddd; + border: 0px; + text-transform: uppercase; + cursor: pointer; + outline: none; } - select:hover { - background-color: #fff; - } +select:hover { + background-color: #fff; +} /* UI */ #resizer { - position: absolute; - z-index: 2; /* Above #sidebar */ - top: 32px; - right: 350px; - width: 5px; - bottom: 0px; - transform: translatex(2.5px); - cursor: col-resize; -} - - #resizer:hover { - background-color: #08f8; - transition-property: background-color; - transition-delay: 0.1s; - transition-duration: 0.2s; - } - - #resizer:active { - background-color: #08f; - } + position: absolute; + z-index: 2; /* Above #sidebar */ + top: 32px; + right: 350px; + width: 5px; + bottom: 0px; + transform: translatex(2.5px); + cursor: col-resize; +} + +#resizer:hover { + background-color: #08f8; + transition-property: background-color; + transition-delay: 0.1s; + transition-duration: 0.2s; +} + +#resizer:active { + background-color: #08f; +} #viewport { - position: absolute; - top: 32px; - left: 0; - right: 350px; - bottom: 0; + position: absolute; + top: 32px; + left: 0; + right: 350px; + bottom: 0; } - #viewport .Text { - text-shadow: 1px 1px 0 rgba(0,0,0,0.25); - pointer-events: none; - } +#viewport .Text { + text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.25); + pointer-events: none; +} #script { - position: absolute; - top: 32px; - left: 0; - right: 350px; - bottom: 0; - opacity: 0.9; + position: absolute; + top: 32px; + left: 0; + right: 350px; + bottom: 0; + opacity: 0.9; } #player { - position: absolute; - top: 32px; - left: 0; - right: 350px; - bottom: 0; + position: absolute; + top: 32px; + left: 0; + right: 350px; + bottom: 0; } #menubar { - position: absolute; - width: 100%; - height: 32px; - background: #eee; - padding: 0; - margin: 0; - right: 0; - top: 0; -} - - #menubar .menu { - float: left; - cursor: pointer; - padding-right: 8px; - } - - #menubar .menu.right { - float: right; - cursor: auto; - padding-right: 0; - text-align: right; - } - - #menubar .menu .title { - display: inline-block; - color: #888; - margin: 0; - padding: 8px; - line-height: 16px; - } - - #menubar .menu .key { - position: absolute; - right: 10px; - color: #ccc; - border: 1px solid #ccc; - border-radius: 4px; - font-size: 9px; - padding: 2px 4px; - right: 10px; - pointer-events: none; - } - - #menubar .menu .options { - position: fixed; - z-index: 1; /* higher than resizer */ - display: none; - padding: 5px 0; - background: #eee; - min-width: 150px; - max-height: calc(100vh - 80px); - overflow: auto; - } - - #menubar .menu:hover .options { - display: block; - box-shadow: 0 10px 10px -5px #00000033; - } - - #menubar .menu .options hr { - border-color: #ddd; - } - - #menubar .menu .options .option { - color: #666; - background-color: transparent; - padding: 5px 10px; - margin: 0 !important; - } - - #menubar .menu .options .option:hover { - color: #fff; - background-color: #08f; - } - - #menubar .menu .options .option:not(.submenu-title):active { - color: #666; - background: transparent; - } - - #menubar .menu .options .option.toggle::before { - - content: ' '; - display: inline-block; - width: 16px; - - } - - #menubar .menu .options .option.toggle-on::before { - - content: '✔'; - font-size: 12px; - - } - - #menubar .submenu-title::after { - content: '⏵'; - float: right; - } - - #menubar .menu .options .inactive { - color: #bbb; - background-color: transparent; - padding: 5px 10px; - margin: 0 !important; - cursor: not-allowed; - } - - - -#sidebar { - position: absolute; - right: 0; - top: 32px; - bottom: 0; - width: 350px; - background: #eee; - overflow: auto; - overflow-x: hidden; + position: absolute; + width: 100%; + height: 32px; + background: #eee; + padding: 0; + margin: 0; + right: 0; + top: 0; } - #sidebar .Panel { - color: #888; - padding: 10px; - border-top: 1px solid #ccc; - } - - #sidebar .Panel.collapsed { - margin-bottom: 0; - } - - #sidebar .Row { - display: flex; - align-items: center; - min-height: 24px; - margin-bottom: 10px; - } - - #sidebar .Row .Label { - - width: 120px; - - } - -#tabs { - background-color: #ddd; - border-top: 1px solid #ccc; +#menubar .menu { + float: left; + cursor: pointer; + padding-right: 8px; } - #tabs span { - color: #aaa; - border-right: 1px solid #ccc; - padding: 10px; - } - - #tabs span.selected { - color: #888; - background-color: #eee; - } - -#toolbar { - position: absolute; - left: 10px; - top: 42px; - width: 32px; - background: #eee; - text-align: center; +#menubar .menu.right { + float: right; + cursor: auto; + padding-right: 0; + text-align: right; } - #toolbar button, #toolbar input { - height: 32px; - } +#menubar .menu .title { + display: inline-block; + color: #888; + margin: 0; + padding: 8px; + line-height: 16px; +} - #toolbar button img { - width: 16px; - opacity: 0.5; - } +#menubar .menu .key { + position: absolute; + right: 10px; + color: #ccc; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 9px; + padding: 2px 4px; + right: 10px; + pointer-events: none; +} -.Outliner { - color: #444; - background-color: #fff; - padding: 0; - width: 100%; - height: 180px; - font-size: 12px; - cursor: default; - overflow: auto; - resize: vertical; - outline: none !important; +#menubar .menu .options { + position: fixed; + z-index: 1; /* higher than resizer */ + display: none; + padding: 5px 0; + background: #eee; + min-width: 150px; + max-height: calc(100vh - 80px); + overflow: auto; } - .Outliner .option { - padding: 4px; - color: #666; - white-space: nowrap; - } +#menubar .menu:hover .options { + display: block; + box-shadow: 0 10px 10px -5px #00000033; +} - .Outliner .option:hover { - background-color: rgba(0,0,0,0.02); - } +#menubar .menu .options hr { + border-color: #ddd; +} - .Outliner .option.active { - background-color: rgba(0,0,0,0.04); - } +#menubar .menu .options .option { + color: #666; + background-color: transparent; + padding: 5px 10px; + margin: 0 !important; +} +#menubar .menu .options .option:hover { + color: #fff; + background-color: #08f; +} -.TabbedPanel .Tabs { - background-color: #ddd; - border-top: 1px solid #ccc; +#menubar .menu .options .option:not(.submenu-title):active { + color: #666; + background: transparent; } - .TabbedPanel .Tab { - color: #aaa; - border-right: 1px solid #ccc; - } +#menubar .menu .options .option.toggle::before { + content: " "; + display: inline-block; + width: 16px; +} - .TabbedPanel .Tab.selected { - color: #888; - background-color: #eee; - } +#menubar .menu .options .option.toggle-on::before { + content: "✔"; + font-size: 12px; +} -.Listbox { - color: #444; - background-color: #fff; +#menubar .submenu-title::after { + content: "⏵"; + float: right; } -.Panel { - color: #888; +#menubar .menu .options .inactive { + color: #bbb; + background-color: transparent; + padding: 5px 10px; + margin: 0 !important; + cursor: not-allowed; } -/* */ +#sidebar { + position: absolute; + right: 0; + top: 32px; + bottom: 0; + width: 350px; + background: #eee; + overflow: auto; + overflow-x: hidden; +} -@media all and ( max-width: 600px ) { +#sidebar .Panel { + color: #888; + padding: 10px; + border-top: 1px solid #ccc; +} - #resizer { - display: none; - } +#sidebar .Panel.collapsed { + margin-bottom: 0; +} - #menubar .menu .options { - max-height: calc(100% - 80px); - } +#sidebar .Row { + display: flex; + align-items: center; + min-height: 24px; + margin-bottom: 10px; +} - #menubar .menu.right { - display: none; - } +#sidebar .Row .Label { + width: 120px; +} - #viewport { - left: 0; - right: 0; - top: 32px; - height: calc(100% - 352px); - } +#tabs { + background-color: #ddd; + border-top: 1px solid #ccc; +} - #script { - left: 0; - right: 0; - top: 32px; - height: calc(100% - 352px); - } +#tabs span { + color: #aaa; + border-right: 1px solid #ccc; + padding: 10px; +} - #player { - left: 0; - right: 0; - top: 32px; - height: calc(100% - 352px); - } +#tabs span.selected { + color: #888; + background-color: #eee; +} - #sidebar { - left: 0; - width: 100%; - top: calc(100% - 320px); - bottom: 0; - } +#toolbar { + position: absolute; + left: 10px; + top: 42px; + width: 32px; + background: #eee; + text-align: center; +} +#toolbar button, +#toolbar input { + height: 32px; } -/* DARK MODE */ +#toolbar button img { + width: 16px; + opacity: 0.5; +} -@media ( prefers-color-scheme: dark ) { +.Outliner { + color: #444; + background-color: #fff; + padding: 0; + width: 100%; + height: 180px; + font-size: 12px; + cursor: default; + overflow: auto; + resize: vertical; + outline: none !important; +} - button { - color: #aaa; - background-color: #222; - } - - button:hover { - color: #ccc; - background-color: #444; - } - - button.selected { - color: #fff; - background-color: #08f; - } - - input, textarea { - background-color: #222; - border: 1px solid transparent; - color: #888; - } - - select { - color: #aaa; - background-color: #222; - } - - select:hover { - color: #ccc; - background-color: #444; - } - - /* UI */ - - #menubar { - background: #111; - } - - #menubar .menu .key { - color: #444; - border-color: #444; - } +.Outliner .option { + padding: 4px; + color: #666; + white-space: nowrap; +} - #menubar .menu .options { - background: #111; - } +.Outliner .option:hover { + background-color: rgba(0, 0, 0, 0.02); +} - #menubar .menu .options hr { - border-color: #222; - } +.Outliner .option.active { + background-color: rgba(0, 0, 0, 0.04); +} - #menubar .menu .options .option { - color: #888; - } +.TabbedPanel .Tabs { + background-color: #ddd; + border-top: 1px solid #ccc; +} - #menubar .menu .options .inactive { - color: #444; - } +.TabbedPanel .Tab { + color: #aaa; + border-right: 1px solid #ccc; +} - #sidebar { - background-color: #111; - } +.TabbedPanel .Tab.selected { + color: #888; + background-color: #eee; +} - #sidebar .Panel { - border-top: 1px solid #222; - } +.Listbox { + color: #444; + background-color: #fff; +} - #sidebar .Panel.Material canvas { - border: solid 1px #5A5A5A; - } +.Panel { + color: #888; +} - #tabs { - background-color: #1b1b1b; - border-top: 1px solid #222; - } +/* */ - #tabs span { - color: #555; - border-right: 1px solid #222; - } +@media all and (max-width: 600px) { + #resizer { + display: none; + } + + #menubar .menu .options { + max-height: calc(100% - 80px); + } + + #menubar .menu.right { + display: none; + } + + #viewport { + left: 0; + right: 0; + top: 32px; + height: calc(100% - 352px); + } + + #script { + left: 0; + right: 0; + top: 32px; + height: calc(100% - 352px); + } + + #player { + left: 0; + right: 0; + top: 32px; + height: calc(100% - 352px); + } + + #sidebar { + left: 0; + width: 100%; + top: calc(100% - 320px); + bottom: 0; + } +} - #tabs span.selected { - background-color: #111; - } +/* DARK MODE */ - #toolbar { - background-color: #111; - } +@media (prefers-color-scheme: dark) { + button { + color: #aaa; + background-color: #222; + } + + button:hover { + color: #ccc; + background-color: #444; + } + + button.selected { + color: #fff; + background-color: #08f; + } + + input, + textarea { + background-color: #222; + border: 1px solid transparent; + color: #888; + } + + select { + color: #aaa; + background-color: #222; + } + + select:hover { + color: #ccc; + background-color: #444; + } + + /* UI */ + + #menubar { + background: #111; + } + + #menubar .menu .key { + color: #444; + border-color: #444; + } + + #menubar .menu .options { + background: #111; + } + + #menubar .menu .options hr { + border-color: #222; + } + + #menubar .menu .options .option { + color: #888; + } + + #menubar .menu .options .inactive { + color: #444; + } + + #sidebar { + background-color: #111; + } + + #sidebar .Panel { + border-top: 1px solid #222; + } + + #sidebar .Panel.Material canvas { + border: solid 1px #5a5a5a; + } + + #tabs { + background-color: #1b1b1b; + border-top: 1px solid #222; + } + + #tabs span { + color: #555; + border-right: 1px solid #222; + } + + #tabs span.selected { + background-color: #111; + } + + #toolbar { + background-color: #111; + } + + #toolbar img { + filter: invert(1); + } + + .Outliner { + background: #222; + } + + .Outliner .option { + color: #999; + } + + .Outliner .option:hover { + background-color: rgba(21, 60, 94, 0.5); + } + + .Outliner .option.active { + background-color: rgba(21, 60, 94, 1); + } + + .TabbedPanel .Tabs { + background-color: #1b1b1b; + border-top: 1px solid #222; + } + + .TabbedPanel .Tabs::-webkit-scrollbar { + background: #111; + } + + .TabbedPanel .Tab { + color: #555; + border-right: 1px solid #222; + } + + .TabbedPanel .Tab.selected { + color: #888; + background-color: #111; + } - #toolbar img { - filter: invert(1); - } + .Listbox { + color: #888; + background: #222; + } - .Outliner { - background: #222; - } + .Listbox .ListboxItem:hover { + background-color: rgba(21, 60, 94, 0.5); + } - .Outliner .option { - color: #999; - } + .Listbox .ListboxItem.active { + background-color: rgba(21, 60, 94, 1); + } +} - .Outliner .option:hover { - background-color: rgba(21,60,94,0.5); - } +/* Temporary Chrome fix (#24794) */ - .Outliner .option.active { - background-color: rgba(21,60,94,1); - } +[draggable="true"] { + transform: translate(0, 0); + z-index: 0; +} - .TabbedPanel .Tabs { - background-color: #1b1b1b; - border-top: 1px solid #222; - } +/* Agent */ +#agent { + position: absolute; + right: 20px; + bottom: 20px; + width: 320px; + padding: 20px; + background-color: #f5f5f5; + border-radius: 12px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); + z-index: 1000; + display: flex; + flex-direction: column; +} - .TabbedPanel .Tabs::-webkit-scrollbar { - background: #111; - } +#agent textarea { + width: 100%; + height: 50px; + padding: 15px; + padding-right: 85px; + background-color: #f5f5f5; + border: 1px solid #e0e0e0; + border-radius: 8px; + color: #333333; + resize: none; + font-size: 14px; + box-sizing: border-box; +} - .TabbedPanel .Tab { - color: #555; - border-right: 1px solid #222; - } +#agent button { + position: absolute; + right: 10px; + bottom: 12px; + background-color: #2196f3; + color: white; + padding: 8px 16px; + border: none; + border-radius: 6px; + cursor: pointer; + font-weight: bold; + font-size: 13px; + transition: background-color 0.2s, opacity 0.2s; +} - .TabbedPanel .Tab.selected { - color: #888; - background-color: #111; - } +#agent button:hover:not(:disabled) { + background-color: #1976d2; +} - .Listbox { - color: #888; - background: #222; - } +#agent button:disabled { + opacity: 0.5; + cursor: default; +} - .Listbox .ListboxItem:hover { - background-color: rgba(21,60,94,0.5); - } +/* Agent steps visualization */ +.agent-steps { + margin-top: 15px; + max-height: 250px; + overflow-y: auto; + padding-top: 10px; +} - .Listbox .ListboxItem.active { - background-color: rgba(21,60,94,1); - } +.agent-step { + display: flex; + margin-bottom: 12px; + animation: fadeIn 0.3s ease-in-out; +} +.agent-step-icon { + flex: 0 0 24px; + font-size: 16px; + margin-right: 8px; } -/* Temporary Chrome fix (#24794) */ +/* Thinking animation */ +.agent-step-icon:has( + + .agent-step-content .agent-step-title:contains("Thinking") + ) { + animation: pulse 1.5s infinite; +} -[draggable="true"] { - transform: translate(0, 0); - z-index: 0; +.agent-step-content { + flex: 1; } -/* Agent */ -#agent { - position: absolute; - left: 50%; - bottom: 20px; - transform: translateX(-50%); - width: 400px; - box-sizing: border-box; - background-color: #ffffff; - padding: 12px; - border-radius: 12px; - color: #333333; - font-family: Arial, sans-serif; - z-index: 1000; - box-shadow: 0 2px 12px rgba(0,0,0,0.1); +.agent-step-title { + font-weight: bold; + font-size: 13px; + margin-bottom: 4px; } -#agent textarea { - width: 100%; - height: 50px; - padding: 15px; - padding-right: 85px; - background-color: #f5f5f5; - border: 1px solid #e0e0e0; - border-radius: 8px; - color: #333333; - resize: none; - font-size: 14px; - box-sizing: border-box; +.agent-step-params { + font-family: monospace; + font-size: 12px; + background-color: rgba(0, 0, 0, 0.05); + padding: 4px 8px; + border-radius: 4px; + word-break: break-all; } -#agent button { - position: absolute; - right: 25px; - bottom: 25px; - background-color: #2196F3; - color: white; - padding: 8px 16px; - border: none; - border-radius: 6px; - cursor: pointer; - font-weight: bold; - font-size: 13px; - transition: background-color 0.2s, opacity 0.2s; +.agent-step-message { + font-size: 13px; + line-height: 1.4; } -#agent button:hover:not(:disabled) { - background-color: #1976D2; +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(5px); + } + to { + opacity: 1; + transform: translateY(0); + } } -#agent button:disabled { - opacity: 0.5; - cursor: default; +@keyframes pulse { + 0% { + opacity: 1; + } + 50% { + opacity: 0.5; + } + 100% { + opacity: 1; + } } /* Dark mode support */ @media (prefers-color-scheme: dark) { - #agent { - background-color: #222; - color: #eee; - box-shadow: 0 2px 12px rgba(0,0,0,0.3); - } - - #agent textarea { - background-color: #333; - border-color: #444; - color: #eee; - } + #agent { + background-color: #222; + color: #eee; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.3); + } + + #agent textarea { + background-color: #333; + border-color: #444; + color: #eee; + } + + .agent-steps { + border-color: #444; + } + + .agent-step-params { + background-color: rgba(255, 255, 255, 0.1); + color: #ccc; + } } diff --git a/editor/js/Agent.js b/editor/js/Agent.js index 5b4aaef4825277..ad6dc5dd84e3f5 100644 --- a/editor/js/Agent.js +++ b/editor/js/Agent.js @@ -1,833 +1,2190 @@ -import { GoogleGenAI } from '@google/genai'; -import * as Commands from './commands/Commands.js'; -import { Vector3, BoxGeometry, SphereGeometry, MeshStandardMaterial, Mesh, DirectionalLight, PointLight, AmbientLight, Color, CylinderGeometry } from 'three'; +// We'll import directly using ES modules with properly named exports +import { GoogleGenAI } from "@google/genai"; +import * as Commands from "./commands/Commands.js"; +import { + Vector3, + BoxGeometry, + SphereGeometry, + MeshStandardMaterial, + Mesh, + DirectionalLight, + PointLight, + AmbientLight, + Color, + CylinderGeometry, + Group, + PlaneGeometry, + TorusGeometry, + RingGeometry, + DodecahedronGeometry, + IcosahedronGeometry, + TetrahedronGeometry, + OctahedronGeometry, + CircleGeometry, + Euler, +} from "../../build/three.module.js"; + +// We'll use the import from the HTML's importmap +// import { GoogleGenerativeAI } from "@google/genai"; class Agent { - - constructor( editor ) { - - this.editor = editor; - this.container = new THREE.Group(); - this.dom = document.createElement( 'div' ); - this.dom.id = 'agent'; - this.lastModifiedObject = null; // Track last modified object - - // Create UI elements - this.createUI(); - - // Initialize signals - this.signals = { - agentResponse: new signals.Signal(), - agentThinking: new signals.Signal() - }; - - // Bind methods - this.processQuery = this.processQuery.bind( this ); - this.executeCommand = this.executeCommand.bind( this ); - this.generateRandomColor = this.generateRandomColor.bind( this ); - this.generateUniqueObjectName = this.generateUniqueObjectName.bind( this ); - - } - - generateUniqueObjectName( baseName ) { - - const scene = this.editor.scene; - let counter = 1; - let name; - - // Keep incrementing counter until we find an unused name - do { - - name = `${baseName}${counter}`; - counter ++; - - } while ( scene.getObjectByName( name ) !== undefined ); - - return name; - - } - - generateRandomColor() { - - const randomHex = Math.floor( Math.random() * 16777215 ).toString( 16 ); - return '#' + randomHex.padStart( 6, '0' ); - - } - - createUI() { - - // Create input area - const input = document.createElement( 'textarea' ); - input.placeholder = 'What do you want to do?'; - - // Prevent keyboard shortcuts when focused - input.addEventListener( 'keydown', ( e ) => { - - e.stopPropagation(); - - if ( e.key === 'Enter' ) { - - if ( e.shiftKey ) { - - // Allow Shift+Enter for newlines - return; - - } - - e.preventDefault(); - executeQuery(); - - } - - } ); - - // Create submit button - const button = document.createElement( 'button' ); - button.textContent = 'SEND'; - - const executeQuery = async () => { - - if ( button.disabled || ! input.value.trim() ) return; - - button.disabled = true; - input.disabled = true; - - await this.processQuery( input.value ); - - input.value = ''; - button.disabled = false; - input.disabled = false; - input.focus(); - - }; - - // Add event listeners - button.addEventListener( 'click', executeQuery ); - - // Append elements - this.dom.appendChild( input ); - this.dom.appendChild( button ); - - } - - async processQuery( query ) { - - if ( ! query.trim() ) return; - - try { - - this.signals.agentThinking.dispatch(); - - // Initialize Google AI - const ai = new GoogleGenAI( { apiKey: 'GEMINI_API_KEY' } ); - - // Get scene information - const sceneInfo = this.getSceneInfo(); - - // Prepare prompt - const prompt = `You are a Three.js scene manipulation assistant. Current scene info: - ${JSON.stringify( sceneInfo, null, 2 )} - - Available commands: - - AddObject: Add a new object to the scene - Types: box/cube, sphere, directionalLight, pointLight, ambientLight, cylinder - Box parameters: - - width, height, depth (default: 1) - - widthSegments, heightSegments, depthSegments (default: 1) - controls geometry detail - Sphere parameters: - - radius (default: 0.5) - - widthSegments (default: 32) - horizontal detail - - heightSegments (default: 16) - vertical detail - Cylinder parameters: - - radiusTop (default: 0.5) - - radiusBottom (default: 0.5) - - height (default: 1) - - radialSegments (default: 32) - horizontal detail - - heightSegments (default: 1) - vertical detail - - openEnded (default: false) - DirectionalLight parameters: - - color (default: white) - - intensity (default: 1) - PointLight parameters: - - color (default: white) - - intensity (default: 1) - - distance (default: 0) - - decay (default: 2) - AmbientLight parameters: - - color (default: white) - - intensity (default: 1) - Common parameters for all: - - color (use simple color names like "red" or hex values like "#ff0000" - do not use functions or dynamic values) - - position (e.g. {x: 0, y: 5, z: 0}) - - SetPosition: Set object position - Parameters: - - object: name of the object to move (optional - defaults to last modified object) - - position: {x, y, z} (omitted coordinates keep current values) - Example: Move right = {x: 2}, Move up = {y: 2} - - SetMaterialColor: Change object material color - Parameters: - - object: name of the object (optional - defaults to last modified object) - - color: color value (e.g. "red", "#ff0000", or "random" for a random color) - Note: Use "random" keyword for random colors, do not use JavaScript expressions - - SetScale: Change object size - Parameters: - - object: name of the object (optional - defaults to last modified object) - - scale: {x, y, z} (values > 1 make bigger, < 1 make smaller) - Example: Double size = {x: 2, y: 2, z: 2} - Example: Half size = {x: 0.5, y: 0.5, z: 0.5} - - SetMaterialValue: Set material property value - Parameters: - - object: name of the object (optional - defaults to last modified object) - - property: material property to set (e.g. "wireframe") - - value: value to set - - SetRotation: Set object rotation - Parameters: - - object: name of the object (optional - defaults to last modified object) - - rotation: {x, y, z} in radians - - SetGeometry: Modify object geometry detail - Parameters: - - object: name of the object to modify (optional - defaults to last modified object) - - widthSegments: number of segments along width (for box/sphere) - - heightSegments: number of segments along height (for box/sphere) - - depthSegments: number of segments along depth (for box only) - Example: High detail sphere = { widthSegments: 64, heightSegments: 32 } - Example: High detail box = { widthSegments: 4, heightSegments: 4, depthSegments: 4 } - - RemoveObject: Remove an object from the scene - Parameters: - - object: name of the object to remove - - MultiCmds: Execute multiple commands in sequence - Parameters: - - commands: array of command objects - Example - Create multiple objects: - { - "type": "MultiCmds", - "params": { - "commands": [ - { - "type": "AddObject", - "params": { - "type": "cube", - "name": "Cube1", - "position": {"x": -1.5} - } - }, - { - "type": "AddObject", - "params": { - "type": "cube", - "name": "Cube2", - "position": {"x": -0.5} - } - }, - { - "type": "AddObject", - "params": { - "type": "cube", - "name": "Cube3", - "position": {"x": 0.5} - } - }, - { - "type": "AddObject", - "params": { - "type": "cube", - "name": "Cube4", - "position": {"x": 1.5} - } - } - ] - } - } - Example - Create and modify an object: - { - "type": "MultiCmds", - "params": { - "commands": [ - { - "type": "AddObject", - "params": { "type": "cube", "name": "MyCube" } - }, - { - "type": "SetMaterialColor", - "params": { "object": "MyCube", "color": "red" } - }, - { - "type": "SetScale", - "params": { "object": "MyCube", "scale": {"x": 2, "y": 2, "z": 2} } - } - ] - } - } - Example - Modify all objects in the scene: - { - "type": "MultiCmds", - "params": { - "commands": [ - { - "type": "SetMaterialColor", - "params": { "object": "Box1", "color": "red" } - }, - { - "type": "SetMaterialColor", - "params": { "object": "Box2", "color": "blue" } - } - ] - } - } - Note: Use MultiCmds when you need to: - 1. Create multiple objects at once - 2. Apply multiple modifications to a single object - 3. Apply modifications to multiple objects - 4. Any combination of the above - - Important: When working with multiple similar objects (e.g. multiple spheres): - - Objects are automatically numbered (e.g. "Sphere1", "Sphere2", etc.) - - Use the exact object name including the number when targeting specific objects - - To modify all objects of a type, create a MultiCmds command with one command per object - - The scene info includes: - - objectCounts: how many of each type exist - - objectsByType: groups of objects by their base name - - spheres: list of all sphere names - - boxes: list of all box names - - cylinders: list of all cylinder names - - directionalLights: list of all directional light names - - pointLights: list of all point light names - - ambientLights: list of all ambient light names - - Example - Set random colors for all spheres: - { - "type": "MultiCmds", - "params": { - "commands": [ - { - "type": "SetMaterialColor", - "params": { "object": "Sphere1", "color": "random" } - }, - { - "type": "SetMaterialColor", - "params": { "object": "Sphere2", "color": "random" } - } - ] - } - } - - User query: ${query} - - Respond ONLY with a JSON object in this format: - { - "response": "Your text response to the user explaining what you're doing", - "commands": { - "type": "command_type", - "params": { - // command specific parameters - } - } - } - - Important: - 1. If no commands are needed, set "commands" to null - 2. Do not include any JavaScript expressions or functions in the JSON - 3. For random colors, use the "random" keyword instead of Math.random() - 4. Do not include any other text outside the JSON - - Do not include any other text outside the JSON.`; - - // Get response - const response = await ai.models.generateContent( { - model: 'gemini-2.0-flash-001', - contents: prompt, - generationConfig: { - temperature: 0.1, // Lower temperature for more consistent JSON output - maxOutputTokens: 2048 - } - } ); - - let responseData; - - try { - - // Strip markdown code block markers if present - const cleanText = response.text.replace( /^```json\n|\n```$/g, '' ) - .replace( /^\s*```\s*|\s*```\s*$/g, '' ) // Remove any remaining code block markers - .trim(); - - try { - - // First try parsing as is - responseData = JSON.parse( cleanText ); - - } catch ( e ) { - - // If that fails, try to fix common JSON issues - const fixedText = cleanText - .replace( /,\s*([}\]])/g, '$1' ) // Remove trailing commas - .replace( /([a-zA-Z0-9])\s*:\s*/g, '"$1": ' ) // Quote unquoted keys - .replace( /\n/g, ' ' ) // Remove newlines - .replace( /\s+/g, ' ' ); // Normalize whitespace - - responseData = JSON.parse( fixedText ); - - } - - } catch ( e ) { - - console.error( 'AGENT: Failed to parse AI response as JSON:', e ); - console.error( 'AGENT: Raw response:', response.text ); - return; - - } - - // Execute commands if present - if ( responseData.commands ) { - - try { - - await this.executeCommand( responseData.commands ); - - } catch ( e ) { - - console.error( 'AGENT: Failed to execute commands:', e ); - - } - - } - - // Log the response - console.log( 'AGENT:', responseData.response ); - this.signals.agentResponse.dispatch( responseData.response ); - - } catch ( error ) { - - console.error( 'AGENT: Agent error:', error ); - - } - - } - - async executeCommand( commandData ) { - - if ( ! commandData.type || ! Commands[ commandData.type + 'Command' ] ) { - - console.error( 'AGENT: Invalid command type:', commandData.type ); - return; - - } - - let command; - - // Helper to get target object, falling back to last modified - const getTargetObject = ( objectName ) => { - - if ( objectName ) { - - const object = this.editor.scene.getObjectByName( objectName ); - if ( object ) { - - this.lastModifiedObject = object; - return object; - - } - - } - - return this.lastModifiedObject; - - }; - - const createMaterial = ( params ) => { - - const material = new MeshStandardMaterial(); - - if ( params.color ) { - - material.color.set( params.color ); - - } - - return material; - - }; - - const setPosition = ( object, position ) => { - - if ( position ) { - - object.position.set( - position.x ?? 0, - position.y ?? 0, - position.z ?? 0 - ); - - } - - }; - - switch ( commandData.type ) { - - case 'AddObject': - - const type = commandData.params.type?.toLowerCase(); - - if ( type === 'box' || type === 'cube' ) { - - const width = commandData.params.width ?? 1; - const height = commandData.params.height ?? 1; - const depth = commandData.params.depth ?? 1; - const widthSegments = commandData.params.widthSegments ?? 1; - const heightSegments = commandData.params.heightSegments ?? 1; - const depthSegments = commandData.params.depthSegments ?? 1; - const geometry = new BoxGeometry( width, height, depth, widthSegments, heightSegments, depthSegments ); - const mesh = new Mesh( geometry, createMaterial( commandData.params ) ); - mesh.name = commandData.params.name || this.generateUniqueObjectName( 'Box' ); - - setPosition( mesh, commandData.params.position ); - - command = new Commands.AddObjectCommand( this.editor, mesh ); - this.lastModifiedObject = mesh; - - } else if ( type === 'sphere' ) { - - const radius = commandData.params.radius ?? 0.5; - const widthSegments = commandData.params.widthSegments ?? 32; - const heightSegments = commandData.params.heightSegments ?? 16; - const geometry = new SphereGeometry( radius, widthSegments, heightSegments ); - const mesh = new Mesh( geometry, createMaterial( commandData.params ) ); - mesh.name = commandData.params.name || this.generateUniqueObjectName( 'Sphere' ); - - setPosition( mesh, commandData.params.position ); - - command = new Commands.AddObjectCommand( this.editor, mesh ); - this.lastModifiedObject = mesh; - - } else if ( type === 'cylinder' ) { - - const radiusTop = commandData.params.radiusTop ?? 0.5; - const radiusBottom = commandData.params.radiusBottom ?? 0.5; - const height = commandData.params.height ?? 1; - const radialSegments = commandData.params.radialSegments ?? 32; - const heightSegments = commandData.params.heightSegments ?? 1; - const openEnded = commandData.params.openEnded ?? false; - const geometry = new CylinderGeometry( radiusTop, radiusBottom, height, radialSegments, heightSegments, openEnded ); - const mesh = new Mesh( geometry, createMaterial( commandData.params ) ); - mesh.name = commandData.params.name || this.generateUniqueObjectName( 'Cylinder' ); - - setPosition( mesh, commandData.params.position ); - - command = new Commands.AddObjectCommand( this.editor, mesh ); - this.lastModifiedObject = mesh; - - } else if ( type === 'directionallight' ) { - - const color = commandData.params.color || 0xffffff; - const intensity = commandData.params.intensity ?? 1; - const light = new DirectionalLight( color, intensity ); - light.name = commandData.params.name || this.generateUniqueObjectName( 'DirectionalLight' ); - - setPosition( light, commandData.params.position ); - - command = new Commands.AddObjectCommand( this.editor, light ); - this.lastModifiedObject = light; - - } else if ( type === 'pointlight' ) { - - const color = commandData.params.color || 0xffffff; - const intensity = commandData.params.intensity ?? 1; - const distance = commandData.params.distance ?? 0; - const decay = commandData.params.decay ?? 2; - const light = new PointLight( color, intensity, distance, decay ); - light.name = commandData.params.name || this.generateUniqueObjectName( 'PointLight' ); - - setPosition( light, commandData.params.position ); - - command = new Commands.AddObjectCommand( this.editor, light ); - this.lastModifiedObject = light; - - } else if ( type === 'ambientlight' ) { - - const color = commandData.params.color || 0xffffff; - const intensity = commandData.params.intensity ?? 1; - const light = new AmbientLight( color, intensity ); - light.name = commandData.params.name || this.generateUniqueObjectName( 'AmbientLight' ); - - command = new Commands.AddObjectCommand( this.editor, light ); - this.lastModifiedObject = light; - - } else { - - console.warn( 'AGENT: Unsupported object type:', type ); - - } - - break; - - case 'SetPosition': - - const positionObject = getTargetObject( commandData.params.object ); - - if ( positionObject && commandData.params.position ) { - - const currentPos = positionObject.position; - const newPosition = new Vector3( - commandData.params.position.x ?? currentPos.x, - commandData.params.position.y ?? currentPos.y, - commandData.params.position.z ?? currentPos.z - ); - command = new Commands.SetPositionCommand( this.editor, positionObject, newPosition ); - - } - - break; - - case 'SetRotation': - - const rotationObject = getTargetObject( commandData.params.object ); - - if ( rotationObject && commandData.params.rotation ) { - - const rot = commandData.params.rotation; - const currentRot = rotationObject.rotation; - const newRotation = new Vector3( - rot.x ?? currentRot.x, - rot.y ?? currentRot.y, - rot.z ?? currentRot.z - ); - command = new Commands.SetRotationCommand( this.editor, rotationObject, newRotation ); - - } - - break; - - case 'SetScale': - - const scaleObject = getTargetObject( commandData.params.object ); - - if ( scaleObject && commandData.params.scale ) { - - const scale = commandData.params.scale; - const newScale = new Vector3( scale.x || 1, scale.y || 1, scale.z || 1 ); - command = new Commands.SetScaleCommand( this.editor, scaleObject, newScale ); - - } - - break; - - case 'SetMaterialColor': - - const colorObject = getTargetObject( commandData.params.object ); - - if ( colorObject && colorObject.material && commandData.params.color ) { - - let colorValue = commandData.params.color; - // If color is "random", generate a random color - if ( colorValue === 'random' ) { - - colorValue = this.generateRandomColor(); - - } - - const color = new Color( colorValue ); - command = new Commands.SetMaterialColorCommand( this.editor, colorObject, 'color', color.getHex() ); - - } - - break; - - case 'SetMaterialValue': - - const materialObject = getTargetObject( commandData.params.object ); - - if ( materialObject && materialObject.material && commandData.params.property ) { - - const value = commandData.params.value ?? true; - command = new Commands.SetMaterialValueCommand( this.editor, materialObject, commandData.params.property, value ); - - } - - break; - - case 'SetGeometry': - - const detailObject = getTargetObject( commandData.params.object ); - - if ( detailObject && detailObject.geometry ) { - - const params = commandData.params; - let newGeometry; - - if ( detailObject.geometry instanceof BoxGeometry ) { - - const box = detailObject.geometry; - newGeometry = new BoxGeometry( - box.parameters.width ?? 1, - box.parameters.height ?? 1, - box.parameters.depth ?? 1, - params.widthSegments ?? 1, - params.heightSegments ?? 1, - params.depthSegments ?? 1 - ); - - } else if ( detailObject.geometry instanceof SphereGeometry ) { - - const sphere = detailObject.geometry; - newGeometry = new SphereGeometry( - sphere.parameters.radius ?? 0.5, - params.widthSegments ?? 32, - params.heightSegments ?? 16 - ); - - } else if ( detailObject.geometry instanceof CylinderGeometry ) { - - const cylinder = detailObject.geometry; - newGeometry = new CylinderGeometry( - params.radiusTop ?? cylinder.parameters.radiusTop ?? 0.5, - params.radiusBottom ?? cylinder.parameters.radiusBottom ?? 0.5, - params.height ?? cylinder.parameters.height ?? 1, - params.radialSegments ?? cylinder.parameters.radialSegments ?? 32, - params.heightSegments ?? cylinder.parameters.heightSegments ?? 1, - params.openEnded ?? cylinder.parameters.openEnded ?? false - ); - - } - - if ( newGeometry ) { - - command = new Commands.SetGeometryCommand( this.editor, detailObject, newGeometry ); - - } - - } - - break; - - case 'RemoveObject': - - const removeObject = getTargetObject( commandData.params.object ); - - if ( removeObject ) { - - command = new Commands.RemoveObjectCommand( this.editor, removeObject ); - this.lastModifiedObject = null; - - } - - break; - - case 'MultiCmds': - - if ( Array.isArray( commandData.params.commands ) ) { - - const commands = []; - - for ( const cmd of commandData.params.commands ) { - - const subCommand = await this.executeCommand( cmd ); - if ( subCommand ) commands.push( subCommand ); - - } - - command = new Commands.MultiCmdsCommand( this.editor, commands ); - - } - - break; - - default: - console.warn( 'AGENT: Unsupported command type:', commandData.type, '- Available commands are: AddObject, SetPosition, SetRotation, SetScale, SetMaterialColor, SetMaterialValue, SetGeometry, RemoveObject, MultiCmds' ); - break; - - } - - console.log( 'AGENT: Command:', command ); - - if ( command ) { - - this.editor.execute( command ); - - } - - return command; - - } - - getSceneInfo() { - - const scene = this.editor.scene; - - // Helper to get all objects of a specific type - const getObjectsByType = ( type ) => { - - return scene.children.filter( obj => { - - const baseName = obj.name.replace( /\d+$/, '' ); - return baseName.toLowerCase() === type.toLowerCase(); - - } ).map( obj => obj.name ); - - }; - - // Get base names and their counts - const nameCount = {}; - const objectsByType = {}; - - scene.children.forEach( obj => { - - const baseName = obj.name.replace( /\d+$/, '' ); // Remove trailing numbers - nameCount[ baseName ] = ( nameCount[ baseName ] || 0 ) + 1; - - // Group objects by their base name - if ( ! objectsByType[ baseName ] ) { - - objectsByType[ baseName ] = []; - - } - - objectsByType[ baseName ].push( obj.name ); - - } ); - - const objects = scene.children.map( obj => ( { - type: obj.type, - name: obj.name, - baseName: obj.name.replace( /\d+$/, '' ), // Add base name - position: obj.position, - rotation: obj.rotation, - scale: obj.scale, - isMesh: obj.isMesh, - isLight: obj.isLight, - material: obj.material ? { - type: obj.material.type, - color: obj.material.color ? '#' + obj.material.color.getHexString() : undefined - } : undefined - } ) ); - - return { - objects, - meshes: objects.filter( obj => obj.isMesh ), - lights: objects.filter( obj => obj.isLight ), - materials: Object.keys( this.editor.materials ).length, - cameras: Object.keys( this.editor.cameras ).length, - objectCounts: nameCount, // Add counts of similar objects - objectsByType, // Add grouped objects by type - spheres: getObjectsByType( 'Sphere' ), - boxes: getObjectsByType( 'Box' ), - cylinders: getObjectsByType( 'Cylinder' ), - directionalLights: getObjectsByType( 'DirectionalLight' ), - pointLights: getObjectsByType( 'PointLight' ), - ambientLights: getObjectsByType( 'AmbientLight' ) - }; - - } - - clear() { - - while ( this.container.children.length > 0 ) { - - this.container.remove( this.container.children[ 0 ] ); - - } - - } - + constructor(editor) { + this.editor = editor; + this.signals = editor.signals; + + // Create custom agent signals + this.agentSignals = { + agentResponse: new signals.Signal(), + agentThinking: new signals.Signal(), + agentStepAdded: new signals.Signal(), + }; + + this.container = new Group(); + this.dom = document.createElement("div"); + this.dom.id = "agent"; + this.lastModifiedObject = null; // Track last modified object + + // Create UI elements + this.createUI(); + + // Bind methods + this.processQuery = this.processQuery.bind(this); + this.generateRandomColor = this.generateRandomColor.bind(this); + this.generateUniqueObjectName = this.generateUniqueObjectName.bind(this); + } + + generateUniqueObjectName(baseName) { + const scene = this.editor.scene; + let counter = 1; + let name; + + // Keep incrementing counter until we find an unused name + do { + name = `${baseName}${counter}`; + counter++; + } while (scene.getObjectByName(name) !== undefined); + + return name; + } + + generateRandomColor() { + const randomHex = Math.floor(Math.random() * 16777215).toString(16); + return "#" + randomHex.padStart(6, "0"); + } + + createUI() { + // Create input area + const input = document.createElement("textarea"); + input.placeholder = "What do you want to do?"; + + // Prevent keyboard shortcuts when focused + input.addEventListener("keydown", (e) => { + e.stopPropagation(); + + if (e.key === "Enter") { + if (e.shiftKey) { + // Allow Shift+Enter for newlines + return; + } + + e.preventDefault(); + executeQuery(); + } + }); + + // Create a container for input and button + const inputContainer = document.createElement("div"); + inputContainer.style.position = "relative"; + inputContainer.appendChild(input); + + // Create submit button + const button = document.createElement("button"); + button.textContent = "SEND"; + + // Position the button inside the input + button.style.position = "absolute"; + button.style.right = "10px"; + button.style.bottom = "12px"; + button.style.zIndex = "1"; + + inputContainer.appendChild(button); + + // Create steps container for visualizing agent steps + const stepsContainer = document.createElement("div"); + stepsContainer.className = "agent-steps"; + + // Store reference to the steps container + this.stepsContainer = stepsContainer; + + const executeQuery = async () => { + if (button.disabled || !input.value.trim()) return; + + button.disabled = true; + input.disabled = true; + + // Clear previous steps + this.stepsContainer.innerHTML = ""; + + await this.processQuery(input.value); + + input.value = ""; + button.disabled = false; + input.disabled = false; + input.focus(); + }; + + // Add event listeners + button.addEventListener("click", executeQuery); + + // Append elements + this.dom.appendChild(inputContainer); + this.dom.appendChild(stepsContainer); + } + + addStep(type, detail) { + const step = document.createElement("div"); + step.className = "agent-step"; + + const icon = document.createElement("span"); + icon.className = "agent-step-icon"; + icon.textContent = + type === "thinking" ? "🤔" : type === "function" ? "⚙️" : "💬"; + + const content = document.createElement("div"); + content.className = "agent-step-content"; + + const title = document.createElement("div"); + title.className = "agent-step-title"; + title.textContent = + type === "thinking" + ? "Thinking..." + : type === "function" + ? `${detail.function}` + : "Response"; + + content.appendChild(title); + + if (detail && type === "function") { + const params = document.createElement("div"); + params.className = "agent-step-params"; + params.textContent = + Object.keys(detail.params || {}).length > 0 + ? JSON.stringify(detail.params) + : "(no parameters)"; + content.appendChild(params); + } else if (type === "response") { + const message = document.createElement("div"); + message.className = "agent-step-message"; + message.textContent = detail; + content.appendChild(message); + } + + step.appendChild(icon); + step.appendChild(content); + this.stepsContainer.appendChild(step); + + // Auto-scroll to the bottom + this.stepsContainer.scrollTop = this.stepsContainer.scrollHeight; + + // Dispatch signal + this.agentSignals.agentStepAdded.dispatch(type, detail); + } + + // Helper method to detect if we're building a multi-part object + isMultiPartObjectRequest(query) { + const multiPartTerms = [ + "car", + "vehicle", + "airplane", + "plane", + "house", + "building", + "robot", + "character", + "person", + "animal", + "furniture", + "chair", + "table", + "snowman", + "tree", + "flower", + "multiple", + "scene", + "environment", + "create a", + "build a", + "make a", + ]; + + return multiPartTerms.some((term) => + query.toLowerCase().includes(term.toLowerCase()) + ); + } + + async processQuery(query) { + if (!query.trim()) return; + + try { + // Show single thinking message at the start + this.agentSignals.agentThinking.dispatch(); + this.addStep("thinking"); + + // Create instance of GoogleGenAI with the Gemini API key + const apiKey = "GEMINI_API_KEY"; + const ai = new GoogleGenAI({ apiKey }); + + console.log( + "AGENT DEBUG: GoogleGenAI SDK version:", + ai.version || "unknown" + ); + + // Get scene information + const sceneInfo = this.getSceneInfo(); + + // Define available functions + const functionDefinitions = this.getFunctionDefinitions(); + + // Prepare system prompt + const systemPrompt = this.getSystemPrompt(sceneInfo); + + // Set up configuration options + const generationConfig = { + temperature: 0.2, + maxOutputTokens: 2048, + }; + + // Tools configuration for function calling + const tools = [ + { + functionDeclarations: functionDefinitions.functionDeclarations, + }, + ]; + + try { + // First send a system message to establish context + await ai.models.generateContent({ + model: "gemini-2.0-flash", + contents: systemPrompt, + generationConfig, + tools, + }); + + console.log( + "AGENT DEBUG: Successfully initialized model and sent system prompt" + ); + + // Create a chat session with history + const chat = ai.chats.create({ + model: "gemini-2.0-flash", + history: [ + { + role: "user", + parts: [{ text: systemPrompt }], + }, + { + role: "model", + parts: [ + { + text: "I'm ready to help you create and modify your 3D scene.", + }, + ], + }, + ], + config: { + temperature: 0.2, + maxOutputTokens: 2048, + tools: [ + { + functionDeclarations: functionDefinitions.functionDeclarations, + }, + ], + }, + }); + + console.log( + "AGENT DEBUG: Chat object created:", + chat, + "Type:", + typeof chat, + "Methods:", + Object.getOwnPropertyNames(Object.getPrototypeOf(chat)) + ); + + // First phase: Planning - get a structured plan for building the requested object + await this.planCreation(chat, query); + } catch (apiError) { + console.error("AGENT: API error:", apiError); + this.addStep( + "response", + "Sorry, I encountered an API error: " + apiError.message + ); + } + } catch (error) { + console.error("AGENT: Agent error:", error); + this.addStep( + "response", + "Sorry, I encountered an error: " + error.message + ); + } + } + + async planCreation(chat, query) { + // Planning phase: Ask the LLM to create a creation plan + const planningQuery = ` +I need to create a 3D model based on this request: "${query}" + +Before creating it, I need you to provide me with a creation plan. Break down the request into: + +1. PARTS INVENTORY: + - List all parts needed to construct the 3D model + - Specify the geometry type for each part (box, cylinder, sphere, etc.) + - Suggest dimensions for each part + +2. SPATIAL ARRANGEMENT: + - Describe how parts should be positioned relative to each other + - Specify orientation (rotations) needed for each part + - Consider which parts should be created first to serve as reference points + +3. ATTRIBUTES: + - Suggest colors or materials for each part + - Specify any special visual characteristics + +4. CONSTRUCTION SEQUENCE: + - Provide a step-by-step order for creating parts + - Start with main/central parts before adding details + +Please be specific about positions (x, y, z coordinates) and orientations (rotations) to ensure parts fit together properly. For complex objects, use precise measurements and rotations. + +Format your plan in a clear, step-by-step way. +`; + + try { + console.log("AGENT: Starting planning phase"); + // No additional 'thinking' step here, as we already have one from processQuery + + // DEBUG: Add chat inspection + console.log( + "AGENT DEBUG: Chat object before sendMessage:", + chat, + "Has sendMessage:", + typeof chat.sendMessage === "function" + ); + + // Prepare the message content + const msgContent = { + role: "user", + parts: [{ text: planningQuery }], + }; + + console.log( + "AGENT DEBUG: Preparing to send message with content:", + JSON.stringify(msgContent) + ); + + try { + // Try different formats as a debugging approach + console.log( + "AGENT DEBUG: Attempting sendMessage with correct params format" + ); + + // FIXED: The sendMessage expects an object with a message property containing the content + const planResult = await chat.sendMessage({ + message: msgContent, + }); + console.log("AGENT: Plan result:", JSON.stringify(planResult, null, 2)); + + // Extract the text content from the plan result - FIX: use .text property, not .text() function + // IMPORTANT: Response from chat.sendMessage returns a GenerateContentResponse object + const planText = + planResult.text || + planResult.candidates?.[0]?.content?.parts?.[0]?.text; + + // Display the plan + if (planText) { + this.agentSignals.agentResponse.dispatch(planText); + this.addStep("response", planText); + + // Second phase: Execution - execute the plan + await this.executeCreation(chat, query, planText); + } else { + this.agentSignals.agentResponse.dispatch( + "I couldn't create a plan for your request." + ); + this.addStep( + "response", + "I couldn't create a plan for your request." + ); + + // Try a fallback simpler approach for common objects + await this.fallbackCreation(query); + } + } catch (attemptError) { + console.error( + "AGENT DEBUG: First attempt failed with error:", + attemptError + ); + console.log("AGENT DEBUG: Error name:", attemptError.name); + console.log("AGENT DEBUG: Error message:", attemptError.message); + console.log("AGENT DEBUG: Error stack:", attemptError.stack); + + // Add a more helpful error message for the user + this.addStep( + "response", + "I encountered an issue while planning your 3D model. Let me try a simpler approach." + ); + + // Try a fallback creation approach for common objects + await this.fallbackCreation(query); + } + } catch (error) { + console.error("AGENT: Planning phase error:", error); + console.log( + "AGENT DEBUG: Full error details:", + "Name:", + error.name, + "Message:", + error.message, + "Stack:", + error.stack + ); + + this.addStep( + "response", + "Error in planning phase: " + + error.message + + ". Let me try a simple approach instead." + ); + + // Try a fallback creation approach for common objects + await this.fallbackCreation(query); + } + } + + // New method for fallback creation when planning fails + async fallbackCreation(query) { + try { + console.log("AGENT: Using fallback creation approach for query:", query); + this.addStep("thinking", "Creating a basic model..."); + + // Analyze the query to determine what basic object to create + const lowerQuery = query.toLowerCase(); + + if ( + lowerQuery.includes("house") || + lowerQuery.includes("home") || + lowerQuery.includes("building") + ) { + // Create a simple house + await this.createSimpleHouse(); + } else if (lowerQuery.includes("car") || lowerQuery.includes("vehicle")) { + // Create a simple car + await this.createSimpleCar(); + } else if (lowerQuery.includes("tree") || lowerQuery.includes("plant")) { + // Create a simple tree + await this.createSimpleTree(); + } else if ( + lowerQuery.includes("person") || + lowerQuery.includes("human") || + lowerQuery.includes("character") + ) { + // Create a simple character + await this.createSimpleCharacter(); + } else { + // Default to a simple cube with some decorations + await this.createSimpleBox(); + } + + this.addStep( + "response", + "I've created a basic model based on your request. You can now modify it further." + ); + } catch (error) { + console.error("AGENT: Fallback creation error:", error); + this.addStep( + "response", + "I couldn't create even a simple model. Please try again with different wording." + ); + } + } + + // Implement simple creation methods for fallback + async createSimpleHouse() { + // Main house body + await this.executeFunction("addObject", { + type: "box", + name: "House Body", + position: { x: 0, y: 2.5, z: 0 }, + color: "#EEEEEE", + width: 10, + height: 5, + depth: 10, + }); + + // Roof + await this.executeFunction("addObject", { + type: "box", + name: "Roof", + position: { x: 0, y: 5.5, z: 0 }, + color: "#8B4513", + width: 12, + height: 1, + depth: 12, + }); + + // Door + await this.executeFunction("addObject", { + type: "box", + name: "Door", + position: { x: 0, y: 1.5, z: 5.1 }, + color: "#8B0000", + width: 2, + height: 3, + depth: 0.2, + }); + + // Window left + await this.executeFunction("addObject", { + type: "box", + name: "Window Left", + position: { x: -3, y: 3, z: 5.1 }, + color: "#ADD8E6", + width: 2, + height: 2, + depth: 0.2, + }); + + // Window right + await this.executeFunction("addObject", { + type: "box", + name: "Window Right", + position: { x: 3, y: 3, z: 5.1 }, + color: "#ADD8E6", + width: 2, + height: 2, + depth: 0.2, + }); + } + + async createSimpleCar() { + // Car body + await this.executeFunction("addObject", { + type: "box", + name: "Car Body", + position: { x: 0, y: 1, z: 0 }, + color: "#FF0000", + width: 5, + height: 1.5, + depth: 2.5, + }); + + // Car cabin + await this.executeFunction("addObject", { + type: "box", + name: "Car Cabin", + position: { x: 0, y: 2, z: 0 }, + color: "#333333", + width: 3, + height: 1, + depth: 2.4, + }); + + // Wheels + await this.executeFunction("addObject", { + type: "cylinder", + name: "Wheel Front Left", + position: { x: -1.5, y: 0.5, z: 1.5 }, + color: "#000000", + radiusTop: 0.5, + radiusBottom: 0.5, + height: 0.5, + radialSegments: 16, + }); + + await this.executeFunction("setRotation", { + object: "Wheel Front Left", + rotation: { x: 1.5707, y: 0, z: 0 }, + }); + + await this.executeFunction("addObject", { + type: "cylinder", + name: "Wheel Front Right", + position: { x: -1.5, y: 0.5, z: -1.5 }, + color: "#000000", + radiusTop: 0.5, + radiusBottom: 0.5, + height: 0.5, + radialSegments: 16, + }); + + await this.executeFunction("setRotation", { + object: "Wheel Front Right", + rotation: { x: 1.5707, y: 0, z: 0 }, + }); + + await this.executeFunction("addObject", { + type: "cylinder", + name: "Wheel Back Left", + position: { x: 1.5, y: 0.5, z: 1.5 }, + color: "#000000", + radiusTop: 0.5, + radiusBottom: 0.5, + height: 0.5, + radialSegments: 16, + }); + + await this.executeFunction("setRotation", { + object: "Wheel Back Left", + rotation: { x: 1.5707, y: 0, z: 0 }, + }); + + await this.executeFunction("addObject", { + type: "cylinder", + name: "Wheel Back Right", + position: { x: 1.5, y: 0.5, z: -1.5 }, + color: "#000000", + radiusTop: 0.5, + radiusBottom: 0.5, + height: 0.5, + radialSegments: 16, + }); + + await this.executeFunction("setRotation", { + object: "Wheel Back Right", + rotation: { x: 1.5707, y: 0, z: 0 }, + }); + } + + async createSimpleTree() { + // Tree trunk + await this.executeFunction("addObject", { + type: "cylinder", + name: "Tree Trunk", + position: { x: 0, y: 2, z: 0 }, + color: "#8B4513", + radiusTop: 0.3, + radiusBottom: 0.5, + height: 4, + radialSegments: 8, + }); + + // Tree top + await this.executeFunction("addObject", { + type: "sphere", + name: "Tree Top 1", + position: { x: 0, y: 5, z: 0 }, + color: "#228B22", + radius: 2, + }); + + await this.executeFunction("addObject", { + type: "sphere", + name: "Tree Top 2", + position: { x: 0, y: 6, z: 0 }, + color: "#228B22", + radius: 1.5, + }); + + await this.executeFunction("addObject", { + type: "sphere", + name: "Tree Top 3", + position: { x: 0, y: 7, z: 0 }, + color: "#228B22", + radius: 1, + }); + } + + async createSimpleCharacter() { + // Head + await this.executeFunction("addObject", { + type: "sphere", + name: "Head", + position: { x: 0, y: 4.5, z: 0 }, + color: "#FFD700", + radius: 0.7, + }); + + // Body + await this.executeFunction("addObject", { + type: "box", + name: "Body", + position: { x: 0, y: 3, z: 0 }, + color: "#FF6347", + width: 1.5, + height: 2, + depth: 0.8, + }); + + // Arms + await this.executeFunction("addObject", { + type: "cylinder", + name: "Left Arm", + position: { x: -1, y: 3.2, z: 0 }, + color: "#FF6347", + radiusTop: 0.2, + radiusBottom: 0.2, + height: 1.5, + radialSegments: 8, + }); + + await this.executeFunction("setRotation", { + object: "Left Arm", + rotation: { x: 0, y: 0, z: 1.5707 }, + }); + + await this.executeFunction("addObject", { + type: "cylinder", + name: "Right Arm", + position: { x: 1, y: 3.2, z: 0 }, + color: "#FF6347", + radiusTop: 0.2, + radiusBottom: 0.2, + height: 1.5, + radialSegments: 8, + }); + + await this.executeFunction("setRotation", { + object: "Right Arm", + rotation: { x: 0, y: 0, z: -1.5707 }, + }); + + // Legs + await this.executeFunction("addObject", { + type: "cylinder", + name: "Left Leg", + position: { x: -0.4, y: 1.2, z: 0 }, + color: "#4682B4", + radiusTop: 0.25, + radiusBottom: 0.25, + height: 1.6, + radialSegments: 8, + }); + + await this.executeFunction("addObject", { + type: "cylinder", + name: "Right Leg", + position: { x: 0.4, y: 1.2, z: 0 }, + color: "#4682B4", + radiusTop: 0.25, + radiusBottom: 0.25, + height: 1.6, + radialSegments: 8, + }); + } + + async createSimpleBox() { + // Just create a colorful box + await this.executeFunction("addObject", { + type: "box", + name: "Colorful Box", + position: { x: 0, y: 1, z: 0 }, + color: this.generateRandomColor(), + width: 2, + height: 2, + depth: 2, + }); + + // Add some decorative elements + await this.executeFunction("addObject", { + type: "sphere", + name: "Decoration 1", + position: { x: 0, y: 2.5, z: 0 }, + color: this.generateRandomColor(), + radius: 0.5, + }); + + await this.executeFunction("addObject", { + type: "torus", + name: "Decoration 2", + position: { x: 0, y: 1, z: 0 }, + color: this.generateRandomColor(), + radius: 1.5, + tube: 0.2, + }); + + await this.executeFunction("setRotation", { + object: "Decoration 2", + rotation: { x: 1.5707, y: 0, z: 0 }, + }); + } + + async executeCreation(chat, query, plan) { + // Execution phase: Execute the creation plan + const executionQuery = ` +I need to create a 3D model based on this request: "${query}" + +Here is the plan I've established: +${plan} + +Now I need to execute this plan. For each part in the plan: +1. Use the addObject function to create the specified geometry +2. Set the position correctly using setPosition +3. Set the rotation as needed using setRotation +4. Set the scale as needed using setScale +5. Set colors using setColor + +For rotations, remember: +- Rotation values are in RADIANS (not degrees) +- Use Math.PI for 180 degrees (e.g., Math.PI/2 for 90 degrees) + +Please execute ONE PART AT A TIME, making sure to follow the position, rotation, and scale specifications from the plan. +Start with the main parts and proceed in a logical sequence. +`; + + try { + console.log("AGENT: Starting execution phase"); + this.addStep("thinking", "Creating 3D model..."); + + // Use the LLM to execute the creation plan with the correct format + await chat.sendMessage({ + message: { + role: "user", + parts: [{ text: executionQuery }], + }, + }); + + // Process the model's response + await this.processModelResponse(chat, "", true); + } catch (error) { + console.error("AGENT: Execution phase error:", error); + this.addStep("response", "Error in execution phase: " + error.message); + } + } + + async processModelResponse(chat, message, isFirstMessage = false) { + try { + // Get updated scene information before sending each message + const currentSceneInfo = this.getSceneInfo(); + + // Only for continuation messages, provide updated scene context + if (!isFirstMessage) { + // Add context about the current scene state for better positioning + message = `Current scene state: ${JSON.stringify( + currentSceneInfo, + null, + 2 + )}\n\n${message}`; + } + + // Send the message + console.log( + `AGENT: ${isFirstMessage ? "Sending" : "Continuing with"} message:`, + message + ); + + // Add retry logic for API calls + let result; + let retryCount = 0; + const maxRetries = 2; + + while (retryCount <= maxRetries) { + try { + // Send message using the SDK with the correct format for content + result = await chat.sendMessage({ + message: { + role: "user", + parts: [{ text: message || " " }], + }, + }); + console.log("AGENT: Raw API response:", result); + + // Check if we have a malformed function call error + const finishReason = result.candidates?.[0]?.finishReason || ""; + if (finishReason === "MALFORMED_FUNCTION_CALL") { + console.log( + "AGENT: Detected malformed function call, retrying with simplified prompt" + ); + + // Simplify the message for retry - keep it generic and based on current scene objects + if (retryCount === 0) { + message = ` +Looking at the current scene, what is the next part to create according to our plan? +Please provide only one function call at a time. + +Current scene objects: ${currentSceneInfo.objectNames.join(", ")} +`; + } else if (retryCount === 1) { + // Even more simplified on second retry + message = `Please continue with the next step in our plan. Use the appropriate function call based on what we need to create next.`; + } else { + // After all retries fail, create a fallback response + console.log("AGENT: All retries failed, using fallback approach"); + + // Determine possible next steps based on scene state + + // Check what we have so far to suggest logical next actions + const existingParts = currentSceneInfo.objectNames; + if ( + existingParts.includes("Roof Part 1") && + !existingParts.includes("Roof Part 2") + ) { + // We have a roof part but need the second part + this.addStep("function", { + function: "addObject", + params: { + type: "box", + name: "Roof Part 2", + position: { x: 0, y: 3.1, z: 0 }, + color: "#8B4513", // Brown + width: 11, + height: 1, + depth: 11, + }, + }); + + await this.executeFunction("addObject", { + type: "box", + name: "Roof Part 2", + position: { x: 0, y: 3.1, z: 0 }, + color: "#8B4513", + width: 11, + height: 1, + depth: 11, + }); + + this.addStep( + "response", + "Added the second roof part. Let me position and rotate it correctly." + ); + + // Now set the rotation for the second roof part + this.addStep("function", { + function: "setRotation", + params: { + object: "Roof Part 2", + rotation: { x: 0.785, y: 0, z: 0 }, + }, + }); + + await this.executeFunction("setRotation", { + object: "Roof Part 2", + rotation: { x: 0.785, y: 0, z: 0 }, + }); + + this.addStep( + "response", + "Completed the roof structure. The house model is now complete!" + ); + return true; + } else if ( + !existingParts.some((name) => name.includes("Chimney")) + ) { + // Maybe add a chimney + this.addStep("function", { + function: "addObject", + params: { + type: "box", + name: "Chimney", + position: { x: 3, y: 4, z: 0 }, + color: "#A52A2A", // Brown/red brick color + width: 1, + height: 2, + depth: 1, + }, + }); + + await this.executeFunction("addObject", { + type: "box", + name: "Chimney", + position: { x: 3, y: 4, z: 0 }, + color: "#A52A2A", + width: 1, + height: 2, + depth: 1, + }); + + this.addStep( + "response", + "Added a chimney to complete the house structure." + ); + return true; + } else { + // Generic fallback - add a lawn/ground + this.addStep("function", { + function: "addObject", + params: { + type: "plane", + name: "Ground", + position: { x: 0, y: 0, z: 0 }, + color: "#7CFC00", // Lawn green + width: 30, + height: 30, + }, + }); + + await this.executeFunction("addObject", { + type: "box", + name: "Ground", + position: { x: 0, y: -0.1, z: 0 }, + color: "#7CFC00", + width: 30, + height: 0.2, + depth: 30, + }); + + this.addStep( + "response", + "Added the ground to complete the scene. The house model is now finished!" + ); + return true; + } + } + + retryCount++; + continue; + } + + // If we get here, the call was successful + break; + } catch (apiError) { + console.error("AGENT: API error during model response:", apiError); + if (retryCount >= maxRetries) { + // After all retries fail, create a fallback response + console.log( + "AGENT: All retries failed due to API errors, using fallback approach" + ); + + // Add a simple completion message + this.addStep( + "response", + "I've encountered some issues continuing with the model creation. The basic structure is in place - you can now modify or add to it manually if needed." + ); + return false; + } + retryCount++; + await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait 1s before retry + } + } + + console.log("AGENT: Raw API response:", JSON.stringify(result, null, 2)); + + // Process function calls if any + const functionCalls = []; + + console.log("AGENT: Checking for function calls in response"); + + // Get the response text - FIX: use .text property, not .text() function + // Access the response text correctly + let responseText = + result.text || result.candidates?.[0]?.content?.parts?.[0]?.text || ""; + + let hasTextContent = responseText ? true : false; + + // Check for function calls in the response + const functionCallParts = result.functionCalls || []; + for (const functionCall of functionCallParts) { + console.log( + "AGENT: Found function call:", + JSON.stringify(functionCall, null, 2) + ); + functionCalls.push(functionCall); + } + + // Extract the text response + console.log("AGENT: Extracted text:", responseText); + + // Process function calls one by one, updating scene context after each call + if (functionCalls.length > 0) { + console.log( + "AGENT: Executing function calls:", + JSON.stringify(functionCalls, null, 2) + ); + + // Execute each function call sequentially + for (const functionCall of functionCalls) { + const functionName = functionCall.name; + const functionArgs = functionCall.args || {}; + + console.log( + "AGENT: Executing function:", + functionName, + "with args:", + JSON.stringify(functionArgs, null, 2) + ); + + this.addStep("function", { + function: functionName, + params: functionArgs, + }); + + // Execute the function and wait for it to complete + await this.executeFunction(functionName, functionArgs); + } + } + + // Display the model's text response + if (hasTextContent) { + this.agentSignals.agentResponse.dispatch(responseText); + this.addStep("response", responseText); + } else if (!functionCalls.length) { + // Only show fallback message if we have neither text content nor function calls + // and this is the first message (not a continuation) + if (isFirstMessage) { + const fallbackText = "Processing your request..."; + this.agentSignals.agentResponse.dispatch(fallbackText); + this.addStep("response", fallbackText); + } + } + + // Determine if we should continue the conversation + const shouldContinue = + (responseText && + (responseText.includes("I'll continue") || + responseText.includes("Let me add") || + responseText.includes("Next, I'll") || + responseText.includes("Now I'll") || + responseText.includes("Next step") || + responseText.includes("Let's add") || + responseText.toLowerCase().includes("next") || + responseText.toLowerCase().includes("continue") || + responseText.toLowerCase().includes("proceed") || + responseText.includes("..."))) || + functionCalls.length > 0; + + // Continue the conversation if there's more to do + if (shouldContinue) { + console.log("AGENT: More steps needed, continuing execution"); + + // Only ask for continuation if we have text or functions were executed + if (hasTextContent || functionCalls.length > 0) { + // Only add thinking step if function calls were executed + if (functionCalls.length > 0) { + this.addStep("thinking", "Continuing to build..."); + } + + try { + // Get updated scene info after function execution + const updatedSceneInfo = this.getSceneInfo(); + + // Send continuation message with updated scene context + const continuationMessage = ` +Current scene state: ${JSON.stringify(updatedSceneInfo, null, 2)} + +Continue executing the rest of the plan. + +IMPORTANT REMINDERS: +1. Look at what's already been created in the scene +2. Determine the next part to add based on our plan +3. Position new parts correctly relative to existing parts +4. Set proper rotations (in radians or degrees) for each part +5. Ensure parts connect properly and maintain correct proportions + +What is the next part to create according to our plan? +`; + await this.processModelResponse(chat, continuationMessage, false); + } catch (error) { + console.error("AGENT: Error continuing execution:", error); + this.addStep( + "response", + "Error while continuing: " + error.message + ); + } + } + } else if (functionCalls.length > 0) { + // If we executed at least one function but shouldn't continue, show completion + console.log("AGENT: Execution complete"); + this.agentSignals.agentResponse.dispatch("Creation complete!"); + this.addStep("response", "Creation complete!"); + } + + return true; + } catch (error) { + console.error("AGENT: Error processing model response:", error); + this.addStep("response", "Error processing response: " + error.message); + return false; + } + } + + async executeFunction(functionName, args) { + try { + console.log( + `AGENT: Executing function ${functionName} with args:`, + JSON.stringify(args, null, 2) + ); + + // Validate args to prevent undefined issues + args = args || {}; + + switch (functionName) { + case "addObject": + console.log("AGENT: Calling handleAddObject"); + // Ensure minimal required properties exist + args.type = args.type || "box"; + args.name = + args.name || + this.generateUniqueObjectName( + args.type.charAt(0).toUpperCase() + args.type.slice(1) + ); + args.position = args.position || { x: 0, y: 0, z: 0 }; + args.color = args.color || "#FFFFFF"; + return await this.handleAddObject(args); + + case "setPosition": + // Ensure we have a position object + args.position = args.position || { x: 0, y: 0, z: 0 }; + return await this.handleSetPosition(args); + + case "setRotation": + // Ensure we have a rotation object + args.rotation = args.rotation || { x: 0, y: 0, z: 0 }; + return await this.handleSetRotation(args); + + case "setScale": + // Ensure we have a scale object + args.scale = args.scale || { x: 1, y: 1, z: 1 }; + return await this.handleSetScale(args); + + case "setMaterialColor": + // Ensure we have a color value + args.color = args.color || "#FFFFFF"; + return await this.handleSetMaterialColor(args); + + case "setMaterialValue": + args.value = args.value || ""; + return await this.handleSetMaterialValue(args); + + case "setGeometry": + return await this.handleSetGeometry(args); + + case "removeObject": + return await this.handleRemoveObject(args); + + default: + console.warn(`AGENT: Unknown function: ${functionName}`); + return { error: `Unknown function: ${functionName}` }; + } + } catch (error) { + console.error(`AGENT: Error executing function ${functionName}:`, error); + + // Try to recover instead of just returning an error + this.addStep( + "response", + `I encountered an error with ${functionName}: ${error.message}. Let me try to continue with the next steps.` + ); + + // Return a graceful error object that won't break the flow + return { error: error.message, recovered: true }; + } + } + + // Helper method to convert degrees to radians + degreesToRadians(degrees) { + return degrees * (Math.PI / 180); + } + + // Helper method to convert radians to degrees + radiansToDegrees(radians) { + return radians * (180 / Math.PI); + } + + // Individual function handlers + + async handleAddObject(args) { + console.log( + "AGENT: Inside handleAddObject with args:", + JSON.stringify(args, null, 2) + ); + const { type, name, position, color, ...geometryParams } = args; + let object; + + // Convert type to lowercase to handle case variations + const objectType = (type || "").toLowerCase(); + console.log("AGENT: Creating object of type:", objectType); + + // Create material with the specified color or default + const material = new MeshStandardMaterial(); + if (color) { + if (color === "random") { + material.color.set(this.generateRandomColor()); + } else { + material.color.set(color); + } + } + + // Generate a unique name if not provided + const objectName = + name || + this.generateUniqueObjectName( + objectType.charAt(0).toUpperCase() + objectType.slice(1) + ); + console.log("AGENT: Using object name:", objectName); + + // Create the geometry based on the type + switch (objectType) { + case "box": + case "cube": + const { + width = 1, + height = 1, + depth = 1, + widthSegments = 1, + heightSegments = 1, + depthSegments = 1, + } = geometryParams; + object = new Mesh( + new BoxGeometry( + width, + height, + depth, + widthSegments, + heightSegments, + depthSegments + ), + material + ); + console.log("AGENT: Created box/cube geometry"); + break; + + case "sphere": + const { + radius = 0.5, + widthSegments: sphereWidthSegments = 32, + heightSegments: sphereHeightSegments = 16, + } = geometryParams; + console.log("AGENT: Creating sphere with params:", { + radius, + sphereWidthSegments, + sphereHeightSegments, + }); + object = new Mesh( + new SphereGeometry(radius, sphereWidthSegments, sphereHeightSegments), + material + ); + console.log("AGENT: Created sphere geometry"); + break; + + case "cylinder": + const { + radiusTop = 0.5, + radiusBottom = 0.5, + height: cylinderHeight = 1, + radialSegments = 32, + heightSegments: cylinderHeightSegments = 1, + openEnded = false, + } = geometryParams; + object = new Mesh( + new CylinderGeometry( + radiusTop, + radiusBottom, + cylinderHeight, + radialSegments, + cylinderHeightSegments, + openEnded + ), + material + ); + break; + + case "plane": + const { width: planeWidth = 1, height: planeHeight = 1 } = + geometryParams; + object = new Mesh(new PlaneGeometry(planeWidth, planeHeight), material); + break; + + case "torus": + const { + radius: torusRadius = 0.5, + tube = 0.2, + radialSegments: torusRadialSegments = 16, + tubularSegments = 100, + } = geometryParams; + object = new Mesh( + new TorusGeometry( + torusRadius, + tube, + torusRadialSegments, + tubularSegments + ), + material + ); + break; + + case "ring": + const { + innerRadius = 0.5, + outerRadius = 1, + thetaSegments = 32, + } = geometryParams; + object = new Mesh( + new RingGeometry(innerRadius, outerRadius, thetaSegments), + material + ); + break; + + case "dodecahedron": + const { radius: dodecahedronRadius = 1 } = geometryParams; + object = new Mesh( + new DodecahedronGeometry(dodecahedronRadius), + material + ); + break; + + case "icosahedron": + const { radius: icosahedronRadius = 1 } = geometryParams; + object = new Mesh(new IcosahedronGeometry(icosahedronRadius), material); + break; + + case "tetrahedron": + const { radius: tetrahedronRadius = 1 } = geometryParams; + object = new Mesh(new TetrahedronGeometry(tetrahedronRadius), material); + break; + + case "octahedron": + const { radius: octahedronRadius = 1 } = geometryParams; + object = new Mesh(new OctahedronGeometry(octahedronRadius), material); + break; + + case "circle": + const { radius: circleRadius = 1, segments = 32 } = geometryParams; + object = new Mesh(new CircleGeometry(circleRadius, segments), material); + break; + + case "group": + object = new Group(); + break; + + case "directionallight": + const { intensity: directionalIntensity = 1 } = geometryParams; + object = new DirectionalLight(color || 0xffffff, directionalIntensity); + break; + + case "pointlight": + const { + intensity: pointIntensity = 1, + distance = 0, + decay = 2, + } = geometryParams; + object = new PointLight( + color || 0xffffff, + pointIntensity, + distance, + decay + ); + break; + + case "ambientlight": + const { intensity: ambientIntensity = 1 } = geometryParams; + object = new AmbientLight(color || 0xffffff, ambientIntensity); + break; + + default: + throw new Error(`Unsupported object type: ${objectType}`); + } + + // Set object name + object.name = objectName; + + // Set position if provided + if (position) { + object.position.set(position.x || 0, position.y || 0, position.z || 0); + } + + // Add object to the scene + const command = new Commands.AddObjectCommand(this.editor, object); + console.log("AGENT: Executing AddObjectCommand with object:", objectName); + this.editor.execute(command); + console.log("AGENT: Command executed successfully"); + + // Update the last modified object + this.lastModifiedObject = object; + console.log("AGENT: Updated lastModifiedObject:", objectName); + + return { objectName }; + } + + async handleSetPosition(args) { + const { object: objectName, position } = args; + + // Get the target object + const object = this.getTargetObject(objectName); + if (!object) { + throw new Error(`Object not found: ${objectName}`); + } + + if (!position) { + throw new Error("Position is required"); + } + + // Get current position + const currentPos = object.position; + + // Create new position vector with existing values for any missing coordinates + const newPosition = new Vector3( + position.x !== undefined ? position.x : currentPos.x, + position.y !== undefined ? position.y : currentPos.y, + position.z !== undefined ? position.z : currentPos.z + ); + + // Execute command + const command = new Commands.SetPositionCommand( + this.editor, + object, + newPosition + ); + this.editor.execute(command); + + return { success: true }; + } + + async handleSetRotation(args) { + const { object: objectName, rotation } = args; + + // Get the target object + const object = this.getTargetObject(objectName); + if (!object) { + throw new Error(`Object not found: ${objectName}`); + } + + if (!rotation) { + throw new Error("Rotation is required"); + } + + // Get current rotation + const currentRot = object.rotation; + + // Check if rotation values are likely in degrees (typically larger values) + // and convert to radians if needed + let rotX = rotation.x !== undefined ? rotation.x : currentRot.x; + let rotY = rotation.y !== undefined ? rotation.y : currentRot.y; + let rotZ = rotation.z !== undefined ? rotation.z : currentRot.z; + + // If values are likely in degrees (typically > 6.28 or < -6.28), convert to radians + if (Math.abs(rotX) > 6.28) rotX = this.degreesToRadians(rotX); + if (Math.abs(rotY) > 6.28) rotY = this.degreesToRadians(rotY); + if (Math.abs(rotZ) > 6.28) rotZ = this.degreesToRadians(rotZ); + + // Create new rotation Euler (not Vector3) and specify 'XYZ' order + const newRotation = new Euler(rotX, rotY, rotZ, "XYZ"); + + // Execute command + const command = new Commands.SetRotationCommand( + this.editor, + object, + newRotation + ); + this.editor.execute(command); + + return { success: true }; + } + + async handleSetScale(args) { + const { object: objectName, scale } = args; + + // Get the target object + const object = this.getTargetObject(objectName); + if (!object) { + throw new Error(`Object not found: ${objectName}`); + } + + if (!scale) { + throw new Error("Scale is required"); + } + + // Create new scale vector + const newScale = new Vector3(scale.x || 1, scale.y || 1, scale.z || 1); + + // Execute command + const command = new Commands.SetScaleCommand(this.editor, object, newScale); + this.editor.execute(command); + + return { success: true }; + } + + async handleSetMaterialColor(args) { + const { object: objectName, color } = args; + + // Get the target object + const object = this.getTargetObject(objectName); + if (!object) { + throw new Error(`Object not found: ${objectName}`); + } + + if (!object.material) { + throw new Error(`Object ${objectName} does not have a material`); + } + + if (!color) { + throw new Error("Color is required"); + } + + // Handle random color + let colorValue = color; + if (color === "random") { + colorValue = this.generateRandomColor(); + } + + // Create color and get hex value + const threeColor = new Color(colorValue); + + // Execute command + const command = new Commands.SetMaterialColorCommand( + this.editor, + object, + "color", + threeColor.getHex() + ); + this.editor.execute(command); + + return { success: true, color: colorValue }; + } + + async handleSetMaterialValue(args) { + const { object: objectName, property, value } = args; + + // Get the target object + const object = this.getTargetObject(objectName); + if (!object) { + throw new Error(`Object not found: ${objectName}`); + } + + if (!object.material) { + throw new Error(`Object ${objectName} does not have a material`); + } + + if (!property) { + throw new Error("Property is required"); + } + + // Convert string value to appropriate type based on property + let convertedValue = value; + + // Convert to boolean for these properties + if ( + ["wireframe", "transparent", "visible", "flatShading"].includes(property) + ) { + convertedValue = value === "true" || value === true; + } + // Convert to number for these properties + else if ( + [ + "opacity", + "metalness", + "roughness", + "refractionRatio", + "aoMapIntensity", + ].includes(property) + ) { + convertedValue = parseFloat(value); + } + + // Execute command + const command = new Commands.SetMaterialValueCommand( + this.editor, + object, + property, + convertedValue + ); + this.editor.execute(command); + + return { success: true }; + } + + async handleSetGeometry(args) { + const { object: objectName, ...geometryParams } = args; + + // Get the target object + const object = this.getTargetObject(objectName); + if (!object) { + throw new Error(`Object not found: ${objectName}`); + } + + if (!object.geometry) { + throw new Error(`Object ${objectName} does not have a geometry`); + } + + let newGeometry; + + // Create appropriate geometry based on the current geometry type + if (object.geometry instanceof BoxGeometry) { + const { widthSegments, heightSegments, depthSegments } = geometryParams; + newGeometry = new BoxGeometry( + object.geometry.parameters.width, + object.geometry.parameters.height, + object.geometry.parameters.depth, + widthSegments || 1, + heightSegments || 1, + depthSegments || 1 + ); + } else if (object.geometry instanceof SphereGeometry) { + const { widthSegments, heightSegments } = geometryParams; + newGeometry = new SphereGeometry( + object.geometry.parameters.radius, + widthSegments || 32, + heightSegments || 16 + ); + } else if (object.geometry instanceof CylinderGeometry) { + const { radialSegments, heightSegments, openEnded } = geometryParams; + newGeometry = new CylinderGeometry( + object.geometry.parameters.radiusTop, + object.geometry.parameters.radiusBottom, + object.geometry.parameters.height, + radialSegments || 32, + heightSegments || 1, + openEnded !== undefined + ? openEnded + : object.geometry.parameters.openEnded + ); + } else { + throw new Error( + `Unsupported geometry type for modification: ${object.geometry.type}` + ); + } + + // Execute command + const command = new Commands.SetGeometryCommand( + this.editor, + object, + newGeometry + ); + this.editor.execute(command); + + return { success: true }; + } + + async handleRemoveObject(args) { + const { object: objectName } = args; + + // Get the target object + const object = this.getTargetObject(objectName); + if (!object) { + throw new Error(`Object not found: ${objectName}`); + } + + // Execute command + const command = new Commands.RemoveObjectCommand(this.editor, object); + this.editor.execute(command); + + // Clear last modified object if it was the removed object + if (this.lastModifiedObject === object) { + this.lastModifiedObject = null; + } + + return { success: true }; + } + + // Helper method to get target object + getTargetObject(objectName) { + if (objectName) { + const object = this.editor.scene.getObjectByName(objectName); + if (object) { + this.lastModifiedObject = object; + return object; + } + } + + return this.lastModifiedObject; + } + + getFunctionDefinitions() { + return { + functionDeclarations: [ + { + name: "addObject", + description: "Add a new object to the scene", + parameters: { + type: "object", + properties: { + type: { + type: "string", + description: + "Type of object to add: box/cube, sphere, cylinder, plane, torus, ring, dodecahedron, icosahedron, tetrahedron, octahedron, circle, group, directionalLight, pointLight, ambientLight", + }, + name: { + type: "string", + description: + "Name for the new object. If not provided, a unique name will be generated.", + }, + position: { + type: "object", + description: "Initial position for the object", + properties: { + x: { type: "number", description: "X coordinate" }, + y: { type: "number", description: "Y coordinate" }, + z: { type: "number", description: "Z coordinate" }, + }, + }, + color: { + type: "string", + description: + "Color for the object material (e.g., 'red', '#ff0000', or 'random')", + }, + width: { + type: "number", + description: "Width for box, plane (default: 1)", + }, + height: { + type: "number", + description: "Height for box, plane, cylinder (default: 1)", + }, + depth: { + type: "number", + description: "Depth for box (default: 1)", + }, + radius: { + type: "number", + description: + "Radius for sphere, torus, ring, dodecahedron, etc. (default: 0.5 for sphere, 1 for others)", + }, + widthSegments: { + type: "integer", + description: + "Width segments for box, sphere (default: 1 for box, 32 for sphere)", + }, + heightSegments: { + type: "integer", + description: + "Height segments for box, sphere (default: 1 for box, 16 for sphere)", + }, + depthSegments: { + type: "integer", + description: "Depth segments for box (default: 1)", + }, + radiusTop: { + type: "number", + description: "Top radius for cylinder (default: 0.5)", + }, + radiusBottom: { + type: "number", + description: "Bottom radius for cylinder (default: 0.5)", + }, + radialSegments: { + type: "integer", + description: + "Radial segments for cylinder, torus (default: 32)", + }, + tube: { + type: "number", + description: "Tube radius for torus (default: 0.2)", + }, + tubularSegments: { + type: "integer", + description: "Tubular segments for torus (default: 100)", + }, + innerRadius: { + type: "number", + description: "Inner radius for ring (default: 0.5)", + }, + outerRadius: { + type: "number", + description: "Outer radius for ring (default: 1)", + }, + thetaSegments: { + type: "integer", + description: "Theta segments for ring (default: 32)", + }, + intensity: { + type: "number", + description: "Intensity for lights (default: 1)", + }, + distance: { + type: "number", + description: "Distance for point light (default: 0)", + }, + decay: { + type: "number", + description: "Decay for point light (default: 2)", + }, + openEnded: { + type: "boolean", + description: "Whether cylinder is open-ended (default: false)", + }, + }, + required: ["type"], + }, + }, + { + name: "setPosition", + description: "Set the position of an object", + parameters: { + type: "object", + properties: { + object: { + type: "string", + description: + "Name of the object to position. If not provided, uses the last modified object.", + }, + position: { + type: "object", + description: + "New position coordinates (omitted coordinates keep current values)", + properties: { + x: { type: "number", description: "X coordinate" }, + y: { type: "number", description: "Y coordinate" }, + z: { type: "number", description: "Z coordinate" }, + }, + }, + }, + required: ["position"], + }, + }, + { + name: "setRotation", + description: + "Set the rotation of an object (can use radians or degrees)", + parameters: { + type: "object", + properties: { + object: { + type: "string", + description: + "Name of the object to rotate. If not provided, uses the last modified object.", + }, + rotation: { + type: "object", + description: + "New rotation values (can be in radians or degrees). Values > 6.28 or < -6.28 are assumed to be degrees and will be converted to radians automatically.", + properties: { + x: { + type: "number", + description: + "Rotation around X axis (pitch - up/down rotation)", + }, + y: { + type: "number", + description: + "Rotation around Y axis (yaw - left/right rotation)", + }, + z: { + type: "number", + description: + "Rotation around Z axis (roll - tilt rotation)", + }, + }, + }, + }, + required: ["rotation"], + }, + }, + { + name: "setScale", + description: "Set the scale of an object", + parameters: { + type: "object", + properties: { + object: { + type: "string", + description: + "Name of the object to scale. If not provided, uses the last modified object.", + }, + scale: { + type: "object", + description: + "New scale factors (values > 1 make bigger, < 1 make smaller)", + properties: { + x: { + type: "number", + description: "Scale factor along X axis", + }, + y: { + type: "number", + description: "Scale factor along Y axis", + }, + z: { + type: "number", + description: "Scale factor along Z axis", + }, + }, + }, + }, + required: ["scale"], + }, + }, + { + name: "setMaterialColor", + description: "Change object material color", + parameters: { + type: "object", + properties: { + object: { + type: "string", + description: + "Name of the object. If not provided, uses the last modified object.", + }, + color: { + type: "string", + description: + "Color value (e.g., 'red', '#ff0000', or 'random' for a random color)", + }, + }, + required: ["color"], + }, + }, + { + name: "setMaterialValue", + description: "Set material property value", + parameters: { + type: "object", + properties: { + object: { + type: "string", + description: + "Name of the object. If not provided, uses the last modified object.", + }, + property: { + type: "string", + description: + "Material property to set (e.g., 'wireframe', 'transparent', 'metalness', 'roughness')", + }, + value: { + type: "string", + description: + "Value to set for the property (will be converted to appropriate type)", + }, + }, + required: ["property"], + }, + }, + { + name: "setGeometry", + description: "Modify object geometry detail", + parameters: { + type: "object", + properties: { + object: { + type: "string", + description: + "Name of the object to modify. If not provided, uses the last modified object.", + }, + widthSegments: { + type: "integer", + description: "Number of segments along width (for box/sphere)", + }, + heightSegments: { + type: "integer", + description: "Number of segments along height (for box/sphere)", + }, + depthSegments: { + type: "integer", + description: "Number of segments along depth (for box only)", + }, + radialSegments: { + type: "integer", + description: "Number of radial segments (for cylinder)", + }, + openEnded: { + type: "boolean", + description: "Whether cylinder is open-ended", + }, + }, + }, + }, + { + name: "removeObject", + description: "Remove an object from the scene", + parameters: { + type: "object", + properties: { + object: { + type: "string", + description: + "Name of the object to remove. If not provided, uses the last modified object.", + }, + }, + }, + }, + ], + }; + } + + getSystemPrompt(sceneInfo) { + return ` +You are a Three.js scene manipulation assistant embedded in the three.js editor. +Your job is to help users create and modify 3D scenes by interpreting natural language requests. + +Current scene information: +${JSON.stringify(sceneInfo, null, 2)} + +IMPORTANT GUIDELINES: + +PLANNING PHASE: +When asked for a plan, provide a detailed breakdown of: +1. All parts required to create the requested object +2. How the parts relate to each other spatially +3. The sequence for building the object (typically starting with the main body/structure) +4. Specific details like colors, dimensions, and proportions + +EXECUTION PHASE: +When executing a plan: +1. Create one part at a time using function calls +2. Set precise positions, scales, and rotations for each part +3. Use meaningful names for each part (e.g., "carBody", "leftWheel", etc.) +4. Follow the plan's sequence exactly + +POSITIONING GUIDANCE: +- Use y-axis for height (y=0 is ground level) +- Use x/z axes for horizontal positioning (x: left/right, z: forward/backward) +- Position parts relative to previously created parts +- Use consistent scale throughout the object +- Ensure parts connect properly (avoid gaps or overlaps) + +ROTATION GUIDANCE: +- Rotations can be specified in either radians (preferred) or degrees +- For degrees, use values like 90, 180, etc. +- For radians, use values like 1.57 (π/2), 3.14 (π), etc. +- Rotation order is XYZ: + * First rotates around X-axis (pitch - up/down) + * Then rotates around Y-axis (yaw - left/right) + * Finally rotates around Z-axis (roll - tilt) +- Common rotations: + * To rotate around X: setRotation({x: value, y: 0, z: 0}) + * To rotate around Y: setRotation({x: 0, y: value, z: 0}) + * To rotate around Z: setRotation({x: 0, y: 0, z: value}) +- To orient objects like wheels correctly, you may need to rotate them 90 degrees + +FUNCTION USAGE: +- Use function calling to manipulate the scene (never return code directly) +- Always specify position parameters when creating objects +- Use color parameters to differentiate parts +- Set appropriate dimensions for each part + +COORDINATE SYSTEM INTERPRETATION: +- "above/on top of" = higher y-value +- "below/under" = lower y-value +- "in front of" = higher z-value +- "behind" = lower z-value +- "to the left of" = lower x-value +- "to the right of" = higher x-value + +Focus on creating visually coherent 3D objects with properly connected parts. +`; + } + + getSceneInfo() { + const scene = this.editor.scene; + + // Get all current objects in the scene + const objects = []; + const objectNames = []; + + scene.traverse((child) => { + if (child.isMesh) { + const position = child.position.clone(); + const rotation = new Euler().copy(child.rotation); + const scale = child.scale.clone(); + + objects.push({ + name: child.name, + type: child.geometry.type, + position: { x: position.x, y: position.y, z: position.z }, + rotation: { x: rotation.x, y: rotation.y, z: rotation.z }, + scale: { x: scale.x, y: scale.y, z: scale.z }, + }); + + objectNames.push(child.name); + } + }); + + return { + objects, + objectNames, + }; + } + + clear() { + while (this.container.children.length > 0) { + this.container.remove(this.container.children[0]); + } + } } export { Agent }; diff --git a/package-lock.json b/package-lock.json index d56dc15952b22a..e6c612bb1ee153 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "three", "version": "0.174.0", "license": "MIT", + "dependencies": { + "@google/genai": "^0.4.0" + }, "devDependencies": { "@rollup/plugin-node-resolve": "^16.0.0", "@rollup/plugin-terser": "^0.4.0", @@ -169,6 +172,19 @@ "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "dev": true }, + "node_modules/@google/genai": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-0.4.0.tgz", + "integrity": "sha512-u9KHoIDbnUi6GpH6mtkZjdeVy3FXI0Hfvl5QWZyYPBttXWaJ13Q4OXE+8zynbHvvGh4XUaH5fBvzsuNLQqB+qQ==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^9.14.2", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -1691,7 +1707,6 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", - "dev": true, "engines": { "node": ">= 14" } @@ -2081,7 +2096,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -2130,6 +2144,15 @@ "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", "dev": true }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -2393,6 +2416,12 @@ "node": "*" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -2982,7 +3011,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, "dependencies": { "ms": "^2.1.3" }, @@ -3264,6 +3292,15 @@ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "dev": true }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -3295,7 +3332,6 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, "optional": true, "dependencies": { "iconv-lite": "^0.6.2" @@ -3999,6 +4035,12 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -4427,6 +4469,36 @@ "node": ">=8" } }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -4628,6 +4700,32 @@ "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", "dev": true }, + "node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -4652,6 +4750,19 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/gzip-size": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", @@ -4845,7 +4956,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, "dependencies": { "agent-base": "^7.1.2", "debug": "4" @@ -4867,7 +4977,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, "optional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -5357,6 +5466,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-string": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", @@ -5614,6 +5735,15 @@ "node": ">=8" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -5671,6 +5801,27 @@ "node >= 0.2.0" ] }, + "node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -6402,8 +6553,7 @@ "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/natural-compare": { "version": "1.4.0", @@ -6429,6 +6579,26 @@ "node": ">= 0.4.0" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -8034,7 +8204,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -8087,7 +8256,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "devOptional": true }, "node_modules/sax": { "version": "1.4.1", @@ -9177,6 +9346,12 @@ "url": "https://github.com/sponsors/Borewit" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -9616,6 +9791,19 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -9653,6 +9841,22 @@ "defaults": "^1.0.3" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -9929,7 +10133,6 @@ "version": "8.18.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", - "dev": true, "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index 387feecb26bc30..6d0b5f5e7ed2fb 100644 --- a/package.json +++ b/package.json @@ -128,5 +128,8 @@ "build/three.module.js" ], "directories": {} + }, + "dependencies": { + "@google/genai": "^0.4.0" } }