Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
107b09e506 | ||
|
|
1f47880e8a | ||
|
|
4920c2a11c | ||
|
|
5dd9cc91bb |
@@ -3,6 +3,7 @@
|
||||
package edit
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
@@ -11,10 +12,81 @@ import (
|
||||
"aego/core/mathx"
|
||||
"aego/editor/core/workspace"
|
||||
"aego/modules/render2d"
|
||||
render2deditor "aego/modules/render2d/editor"
|
||||
"aego/scene"
|
||||
"aego/scene/frame"
|
||||
)
|
||||
|
||||
// The toolbar must leave the debug image, not just clear its checked state.
|
||||
func TestSceneViewLightingToggleClearsDebugPixels(t *testing.T) {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
p := newProject(t)
|
||||
const path = "scenes/debug-toggle.aescene"
|
||||
writeScene(t, p, path, func(s *scene.Scene) {
|
||||
h := s.Create(s.Root(), "Red")
|
||||
sr := scene.Add[render2d.SpriteRenderer](s, h)
|
||||
*sr = render2d.DefaultSpriteRenderer()
|
||||
sr.Sprite = asset.NewRef[asset.Texture](asset.BuiltinWhite)
|
||||
sr.Material = asset.NewRef[asset.Material](asset.BuiltinMaterialLit)
|
||||
sr.Tint = mathx.Color{R: 1, A: 1}
|
||||
sr.Size = mathx.V2(4, 4)
|
||||
})
|
||||
s, err := Open(p, Config{Width: 800, Height: 600, Hidden: true, NoWatch: true, Logger: log.Nop()})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer s.Close()
|
||||
s.Step()
|
||||
ed := s.App.Context()
|
||||
doc, err := ed.Docs.Open(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
doc.View.Camera.Position = mathx.Vec2{}
|
||||
s.App.SceneView().SetZoom(40)
|
||||
ed.Settings.Grid = false
|
||||
for range 3 {
|
||||
s.Step()
|
||||
}
|
||||
panel := s.App.SceneView()
|
||||
raw, ok := panel.LightingWorld().ServiceAny(frame.ServiceID)
|
||||
if !ok {
|
||||
t.Fatal("missing Scene View runner")
|
||||
}
|
||||
runner := raw.(*frame.Runner)
|
||||
read := func() []byte {
|
||||
t.Helper()
|
||||
for range 2 {
|
||||
s.Step()
|
||||
}
|
||||
target, w, h := runner.Target()
|
||||
pixels := make([]byte, w*h*4)
|
||||
if target.IsZero() || ed.Renderer.Device().ReadPixels(target, 0, 0, w, h, pixels) != nil {
|
||||
t.Fatal("cannot read the Scene View")
|
||||
}
|
||||
return pixels
|
||||
}
|
||||
press := func(id string) {
|
||||
t.Helper()
|
||||
if err := ed.Commands.Run(id); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
press(render2deditor.CmdLighting)
|
||||
unlit := read()
|
||||
for _, debug := range []string{render2deditor.CmdLightingNormals, render2deditor.CmdLightingShadows} {
|
||||
press(debug)
|
||||
if bytes.Equal(unlit, read()) {
|
||||
t.Fatalf("%s did not change the rendered image", debug)
|
||||
}
|
||||
press(render2deditor.CmdLighting)
|
||||
if off := read(); !bytes.Equal(unlit, off) {
|
||||
t.Fatalf("turning off lighting left %s in the rendered image", debug)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLightingAuthoringWorkflowNative(t *testing.T) {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
|
||||
+1
-2
@@ -50,8 +50,7 @@ func ShaderProblems(imps *pipeline.ImporterRegistry) problems.Source {
|
||||
if si == nil {
|
||||
return 0
|
||||
}
|
||||
_, rev := si.Diagnostics()
|
||||
return rev
|
||||
return si.Revision()
|
||||
},
|
||||
Append: func(dst []problems.Diagnostic) []problems.Diagnostic {
|
||||
if si == nil {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package shader
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"aego/render/shaderx"
|
||||
)
|
||||
|
||||
func TestDiagnosticRevisionDoesNotAllocate(t *testing.T) {
|
||||
i := New()
|
||||
i.record("assets/a.aeshader", []shaderx.Diag{{Code: shaderx.SHD001}})
|
||||
_, before := i.Diagnostics()
|
||||
if before == 0 || i.Revision() != before {
|
||||
t.Fatal("diagnostic revision was not reported")
|
||||
}
|
||||
if n := testing.AllocsPerRun(100, func() { _ = i.Revision() }); n != 0 {
|
||||
t.Fatalf("checking the revision allocates %v times", n)
|
||||
}
|
||||
i.record("assets/a.aeshader", nil)
|
||||
if i.Revision() == before {
|
||||
t.Fatal("clearing a diagnostic did not advance the revision")
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,13 @@ func (i *Importer) Diagnostics() ([]shaderx.Diag, uint64) {
|
||||
return out, i.rev
|
||||
}
|
||||
|
||||
// Revision allows consumers to check for changes without rebuilding the sorted diagnostics.
|
||||
func (i *Importer) Revision() uint64 {
|
||||
i.mu.Lock()
|
||||
defer i.mu.Unlock()
|
||||
return i.rev
|
||||
}
|
||||
|
||||
func (i *Importer) record(path string, diags []shaderx.Diag) {
|
||||
i.mu.Lock()
|
||||
defer i.mu.Unlock()
|
||||
|
||||
@@ -25,6 +25,13 @@ func iconsUsage() string {
|
||||
|
||||
func editorIcons() []module.IconUse {
|
||||
out := iconuse.Uses()
|
||||
// Module-provided toolbar icons belong in the application composition, not
|
||||
// in the module-independent editor's icon registry.
|
||||
out = append(out,
|
||||
module.IconUse{ID: "render2d.normals", Owner: iconuse.Owner, Size: 16, By: "Scene View: surface normals"},
|
||||
module.IconUse{ID: "render2d.point_light", Owner: iconuse.Owner, Size: 16, By: "Scene View: lighting preview"},
|
||||
module.IconUse{ID: "render2d.shadow_view", Owner: iconuse.Owner, Size: 16, By: "Scene View: shadow visibility"},
|
||||
)
|
||||
for _, t := range projects.Templates {
|
||||
if t.Icon != "" {
|
||||
out = append(out, module.IconUse{ID: t.Icon, Owner: iconuse.Owner, By: "New Project: template " + t.ID, Size: 24})
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -31,3 +32,23 @@ func TestIconsCheckReportsMissingEditorIcon(t *testing.T) {
|
||||
t.Fatalf("a missing editor icon must fail the check:\n%s", b.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestIconsIncludeModuleToolbarUses(t *testing.T) {
|
||||
report, err := iconReport()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := map[string]string{
|
||||
"render2d.normals": "Scene View: surface normals",
|
||||
"render2d.point_light": "Scene View: lighting preview",
|
||||
"render2d.shadow_view": "Scene View: shadow visibility",
|
||||
}
|
||||
for _, row := range report.Rows() {
|
||||
if by, ok := want[row.ID]; ok && slices.Contains(row.UsedBy, by) {
|
||||
delete(want, row.ID)
|
||||
}
|
||||
}
|
||||
if len(want) != 0 {
|
||||
t.Fatalf("module toolbar icons missing from the audit: %v", want)
|
||||
}
|
||||
}
|
||||
|
||||
+13
-4
@@ -19,6 +19,8 @@ Sources are `<id>.svg` files in `engine/res/icons/src` and `modules/<id>/res/ico
|
||||
| `anim.slice` | engine | 16 | Create palette: Animation → Flipbook from Selection; PNG inspector: Slicing section | ok |
|
||||
| `anim.to_end` | engine | 16 | Animation panel: go to end | ok |
|
||||
| `anim.to_start` | engine | 16 | Animation panel: go to start | ok |
|
||||
| `animation.animator` | engine | 16 | component animation.Animator | ok |
|
||||
| `animation.player` | engine | 16 | component animation.AnimationPlayer | ok |
|
||||
| `app` | engine | 16 | launcher header; colored PNGs in app/brand are the window, installer and favicon icon | ok |
|
||||
| `app.quit` | engine | 16 | File → Quit | ok |
|
||||
| `audio.listener` | engine | 16 | component audio.AudioListener2D | ok |
|
||||
@@ -36,7 +38,7 @@ Sources are `<id>.svg` files in `engine/res/icons/src` and `modules/<id>/res/ico
|
||||
| `cat.node` | engine | 24 | New Project: template empty; Search: node results; Add Component and Create: category | ok |
|
||||
| `cat.particles` | engine | 16 | Add Component and Create: Effects category; template menu Effects | ok |
|
||||
| `cat.physics` | engine | 16 | template menu Physics | ok |
|
||||
| `cat.render2d` | render2d | 16 | template menu Lighting | missing |
|
||||
| `cat.render2d` | engine | 16 | Add Component and Create: Lights category; template menu Lighting | ok |
|
||||
| `cat.rendering` | engine | 24 | New Project: template 2d; Search: mesh assets; Add Component and Create: category | ok |
|
||||
| `cat.scripts` | engine | 16 | Search: script symbols; Add Component and Create: category | ok |
|
||||
| `cat.settings` | engine | 16 | References: the project file; Add Component and Create: category | ok |
|
||||
@@ -58,9 +60,9 @@ Sources are `<id>.svg` files in `engine/res/icons/src` and `modules/<id>/res/ico
|
||||
| `comp.persist` | engine | 16 | component save.Persist | ok |
|
||||
| `comp.polygon` | engine | 16 | Polygon shape component; Shapes debug overlay; component physics.PolygonCollider2D | ok |
|
||||
| `comp.rigidbody` | engine | 16 | component physics.Body2D; template physics.rigidbody | ok |
|
||||
| `comp.script` | engine | 16 | Scripts component; Assets menu: Attach to Selected; component engine.Script | ok |
|
||||
| `comp.script` | engine | 16 | Scripts component and Add Component entries; Assets menu: Attach to Selected; component engine.Script | ok |
|
||||
| `comp.shake` | engine | 16 | component camera.CameraShake2D | ok |
|
||||
| `comp.sprite` | engine | 16 | Create: Material and Material from Shader; material asset inspector; component engine.Light2D; component engine.MaterialOverride; component engine.SpriteRenderer; template render2d.directional-light; template render2d.point-light; template render2d.spot-light; template render2d.sprite; template render2d.sprite-light | ok |
|
||||
| `comp.sprite` | engine | 16 | Create: Material and Material from Shader; material asset inspector; component engine.MaterialOverride; component engine.SpriteRenderer; template render2d.sprite | ok |
|
||||
| `comp.tags` | engine | 16 | component engine.Tags | ok |
|
||||
| `comp.vcam` | engine | 16 | component camera.VirtualCamera2D | ok |
|
||||
| `comp.zone` | engine | 16 | component camera.CameraZone2D | ok |
|
||||
@@ -92,6 +94,7 @@ Sources are `<id>.svg` files in `engine/res/icons/src` and `modules/<id>/res/ico
|
||||
| `file.autosave` | engine | 16 | File → Autosave Scripts | ok |
|
||||
| `file.data` | engine | 24 | Assets: data asset tiles | ok |
|
||||
| `file.download` | engine | 16 | Documentation: Install and run page; PNG inspector: Import section | ok |
|
||||
| `file.export` | engine | 16 | File → Export and the Export panel | ok |
|
||||
| `file.folder` | engine | 24 | Assets: folders | ok |
|
||||
| `file.mesh` | engine | 24 | Assets: mesh tiles | ok |
|
||||
| `file.open` | engine | 16 | Assets menu: Open | ok |
|
||||
@@ -160,7 +163,7 @@ Sources are `<id>.svg` files in `engine/res/icons/src` and `modules/<id>/res/ico
|
||||
| `play.copy_back` | engine | 16 | Component menu during Play: Copy values to edit scene | ok |
|
||||
| `play.pause` | engine | 16 | toolbar: Pause; Animation panel: pause the clip | ok |
|
||||
| `play.speed` | engine | 16 | Play → Time Scale… | ok |
|
||||
| `play.start` | engine | 16 | component animation.AnimationPlayer; component animation.Animator; toolbar: Play; Assets: startup scene mark; Animation panel: play the clip | ok |
|
||||
| `play.start` | engine | 16 | toolbar: Play; Assets: startup scene mark; Animation panel: play the clip | ok |
|
||||
| `play.step` | engine | 16 | toolbar: Step | ok |
|
||||
| `play.step_back` | engine | 16 | Play → Step Back | ok |
|
||||
| `play.stop` | engine | 16 | toolbar: Stop | ok |
|
||||
@@ -168,6 +171,12 @@ Sources are `<id>.svg` files in `engine/res/icons/src` and `modules/<id>/res/ico
|
||||
| `prefab.revert` | engine | 16 | Prefab menus: Revert and Reset to Prefab Value | ok |
|
||||
| `prefab.unpack` | engine | 16 | Hierarchy prefab menu: Unpack | ok |
|
||||
| `prefab.variant` | engine | 16 | Assets menu: Create Variant | ok |
|
||||
| `render2d.directional_light` | engine | 16 | template render2d.directional-light | ok |
|
||||
| `render2d.normals` | engine | 16 | Scene View: surface normals | ok |
|
||||
| `render2d.point_light` | engine | 16 | Scene View: lighting preview; component engine.Light2D; template render2d.point-light | ok |
|
||||
| `render2d.shadow_view` | engine | 16 | Scene View: shadow visibility | ok |
|
||||
| `render2d.spot_light` | engine | 16 | template render2d.spot-light | ok |
|
||||
| `render2d.sprite_light` | engine | 16 | template render2d.sprite-light | ok |
|
||||
| `shape.cube` | engine | 16 | template render3d.cube | ok |
|
||||
| `shape.cylinder` | engine | 16 | template render3d.cylinder | ok |
|
||||
| `shape.dice` | engine | 16 | Documentation: random numbers module | ok |
|
||||
|
||||
@@ -20,6 +20,7 @@ func MenuItem(ui *core.Context, reg *Registry, id, label string) bool {
|
||||
Tip: d.TipText(),
|
||||
Enabled: reg.Enabled(id),
|
||||
Checked: d.Checked != nil && d.Checked(),
|
||||
KeepOpen: d.Checked != nil,
|
||||
}
|
||||
if widgets.Entry(ui, it) {
|
||||
reg.Run(id)
|
||||
|
||||
@@ -40,6 +40,7 @@ var Editor = []module.IconUse{
|
||||
use("cat.engine", 14, "Agent panel: steps that read component types"),
|
||||
use("cat.node", 16, "Search: node results; Add Component and Create: category"),
|
||||
use("cat.particles", 16, "Add Component and Create: Effects category"),
|
||||
use("cat.render2d", 16, "Add Component and Create: Lights category"),
|
||||
use("cat.rendering", 16, "Search: mesh assets; Add Component and Create: category"),
|
||||
use("cat.scripts", 16, "Search: script symbols; Add Component and Create: category"),
|
||||
use("cat.settings", 16, "References: the project file; Add Component and Create: category"),
|
||||
@@ -50,7 +51,7 @@ var Editor = []module.IconUse{
|
||||
use("comp.joint_revolute", 16, "Revolute Joint 2D component; Joints debug overlay"),
|
||||
use("comp.polygon", 16, "Polygon shape component; Shapes debug overlay"),
|
||||
use("comp.sprite", 16, "Create: Material and Material from Shader; material asset inspector"),
|
||||
use("comp.script", 16, "Scripts component; Assets menu: Attach to Selected"),
|
||||
use("comp.script", 16, "Scripts component and Add Component entries; Assets menu: Attach to Selected"),
|
||||
use("dbg.aabbs", 16, "AABBs debug overlay"),
|
||||
use("dbg.broadphase", 16, "Broadphase Tree debug overlay"),
|
||||
use("dbg.contacts", 16, "Contacts debug overlay; Vectors → Contact Impulses"),
|
||||
@@ -78,6 +79,7 @@ var Editor = []module.IconUse{
|
||||
use("file.audio", 16, "Documentation: sound clips page"),
|
||||
use("file.autosave", 16, "File → Autosave Scripts"),
|
||||
use("file.download", 16, "Documentation: Install and run page; PNG inspector: Import section"),
|
||||
use("file.export", 16, "File → Export and the Export panel"),
|
||||
use("file.data", 24, "Assets: data asset tiles"),
|
||||
use("file.folder", 24, "Assets: folders"),
|
||||
use("file.mesh", 24, "Assets: mesh tiles"),
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"image/png"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"aego/editor/core/code/highlight"
|
||||
)
|
||||
|
||||
type glyph struct {
|
||||
@@ -91,6 +93,51 @@ func (s *Service) card(header string, lines []string) pixels {
|
||||
return raster(im)
|
||||
}
|
||||
|
||||
// codeCard draws code as colored strokes rather than shrinking bitmap glyphs
|
||||
// into illegible noise at the 48–96px sizes of the asset browser.
|
||||
func codeCard(lines []string) pixels {
|
||||
im := canvas()
|
||||
draw.Draw(im, image.Rect(0, 0, Size, 5), image.NewUniform(accent), image.Point{}, draw.Src)
|
||||
palette := [highlight.KindCount]color.NRGBA{}
|
||||
for i := range palette {
|
||||
palette[i] = color.NRGBA{175, 186, 197, 255}
|
||||
}
|
||||
palette[highlight.Keyword] = color.NRGBA{180, 145, 220, 255}
|
||||
palette[highlight.Script] = color.NRGBA{180, 145, 220, 255}
|
||||
palette[highlight.String] = color.NRGBA{165, 200, 140, 255}
|
||||
palette[highlight.Number] = color.NRGBA{220, 165, 115, 255}
|
||||
palette[highlight.Comment] = color.NRGBA{105, 130, 130, 255}
|
||||
palette[highlight.Type] = color.NRGBA{115, 190, 210, 255}
|
||||
var state highlight.State
|
||||
var spans []highlight.Span
|
||||
for lineNo, line := range lines {
|
||||
y := 13 + lineNo*9
|
||||
if y+3 >= Size-3 {
|
||||
break
|
||||
}
|
||||
spans, state = highlight.Line([]byte(line), state, spans)
|
||||
for _, sp := range spans {
|
||||
start := -1
|
||||
for i := sp.Start; i <= sp.End; i++ {
|
||||
if i < sp.End && line[i] != ' ' && line[i] != '\t' {
|
||||
if start < 0 {
|
||||
start = i
|
||||
}
|
||||
continue
|
||||
}
|
||||
if start >= 0 {
|
||||
x0, x1 := 7+start*3, min(7+i*3, Size-7)
|
||||
if x0 < x1 {
|
||||
draw.Draw(im, image.Rect(x0, y, x1, y+3), image.NewUniform(palette[sp.Kind]), image.Point{}, draw.Src)
|
||||
}
|
||||
start = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return raster(im)
|
||||
}
|
||||
|
||||
func fitPixels(im *image.NRGBA, area image.Rectangle, p pixels) {
|
||||
if p.w < 1 || p.h < 1 {
|
||||
return
|
||||
|
||||
@@ -257,10 +257,13 @@ func (s *Service) startCard(e asset.Entry, it *item, spec tasks.Spec, bad bool)
|
||||
reason = "GPU preview unavailable in this session"
|
||||
}
|
||||
it.task = s.d.Tasks.Work(spec, func(*tasks.Task) error {
|
||||
cached := false
|
||||
if !bad && reason == "" {
|
||||
if cached, err := s.load(key); err == nil {
|
||||
out = cached
|
||||
return nil
|
||||
if old, err := s.load(key); err == nil {
|
||||
out, cached = old, true
|
||||
if !codeEntry(e) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,6 +289,9 @@ func (s *Service) startCard(e asset.Entry, it *item, spec tasks.Spec, bad bool)
|
||||
}
|
||||
|
||||
lines = cardLines(e, src)
|
||||
if cached {
|
||||
return nil
|
||||
}
|
||||
if len(src) == 0 && len(e.Artifacts) > 0 && s.d.Pipeline != nil {
|
||||
a := e.Artifacts[0]
|
||||
if f, err := s.d.Pipeline.Artifacts().Open(a.Path); err == nil {
|
||||
@@ -315,7 +321,11 @@ func (s *Service) startCard(e asset.Entry, it *item, spec tasks.Spec, bad bool)
|
||||
if reason != "" {
|
||||
lines = append([]string{"! " + reason}, lines...)
|
||||
}
|
||||
out = s.card(string(e.Kind), lines)
|
||||
if codeEntry(e) && reason == "" && len(src) > 0 {
|
||||
out = codeCard(lines)
|
||||
} else {
|
||||
out = s.card(string(e.Kind), lines)
|
||||
}
|
||||
if reason == "" {
|
||||
return s.save(key, out)
|
||||
}
|
||||
@@ -332,6 +342,10 @@ func (s *Service) startCard(e asset.Entry, it *item, spec tasks.Spec, bad bool)
|
||||
})
|
||||
}
|
||||
|
||||
func codeEntry(e asset.Entry) bool {
|
||||
return e.Kind == asset.ArtScript || e.Kind == SourceKind && strings.EqualFold(filepath.Ext(e.Source), ".go")
|
||||
}
|
||||
|
||||
func textKind(e asset.Entry) bool {
|
||||
switch strings.ToLower(filepath.Ext(e.Source)) {
|
||||
case ".go", ".aes", ".aeshader", ".glsl", ".vert", ".frag", ".aedata", ".aetable", ".aepreset", ".aectrl", ".aeanim", ".aemat", ".aescene", ".aeprefab", ".aego", ".aetext", ".json", ".csv", ".toml":
|
||||
@@ -343,21 +357,6 @@ func textKind(e asset.Entry) bool {
|
||||
func cardLines(e asset.Entry, src []byte) []string {
|
||||
if len(src) > 0 && !bytes.ContainsRune(src, 0) && textKind(e) {
|
||||
ls := firstLines(string(src), lineCount)
|
||||
if e.Kind == asset.ArtScript || filepath.Ext(e.Source) == ".go" {
|
||||
var decl []string
|
||||
for _, line := range strings.Split(string(src), "\n") {
|
||||
l := strings.TrimSpace(line)
|
||||
for _, k := range []string{"script ", "func ", "type "} {
|
||||
if strings.HasPrefix(l, k) {
|
||||
decl = append(decl, strings.TrimSuffix(l, " {"))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(decl) > 0 {
|
||||
ls = decl
|
||||
}
|
||||
}
|
||||
if len(ls) > 0 {
|
||||
return ls
|
||||
}
|
||||
|
||||
@@ -174,6 +174,9 @@ func (s *Service) Key(e asset.Entry) (string, bool) {
|
||||
}
|
||||
h := sha256.New()
|
||||
fmt.Fprintf(h, "%s:%d:%s:%s", salt, Size, s.handlers[e.Kind].Version, e.Kind)
|
||||
if codeEntry(e) {
|
||||
fmt.Fprint(h, "|code-preview-v2")
|
||||
}
|
||||
if s.handlers[e.Kind].Render != nil && !s.gpuOK {
|
||||
fmt.Fprint(h, "|no-gpu")
|
||||
}
|
||||
@@ -296,7 +299,7 @@ func (s *Service) Missing(info pipeline.AssetInfo) bool {
|
||||
}
|
||||
|
||||
func (s *Service) Lines(info pipeline.AssetInfo) []string {
|
||||
if info.Kind != asset.ArtScript {
|
||||
if !codeEntry(asset.Entry{Kind: info.Kind, Source: info.Path}) {
|
||||
return nil
|
||||
}
|
||||
if it := s.ensure(info); it != nil {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"aego/render/rt"
|
||||
"aego/render/view"
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"aego/asset"
|
||||
@@ -85,3 +86,23 @@ func TestFirstLinesSkipImports(t *testing.T) {
|
||||
t.Fatalf("lines = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScriptPreviewUsesSourceAndColoredStrokes(t *testing.T) {
|
||||
src := []byte("package game\n\nscript Player {\n var Speed float = 10\n func Update() {\n Speed = 20\n }\n}\n")
|
||||
e := asset.Entry{Kind: asset.ArtScript, Source: "scripts/player.aes"}
|
||||
lines := cardLines(e, src)
|
||||
if len(lines) < 3 || lines[0] != "script Player {" || !strings.Contains(strings.Join(lines, "\n"), "Speed = 20") {
|
||||
t.Fatalf("preview lost the script contents: %q", lines)
|
||||
}
|
||||
p := codeCard(lines)
|
||||
if p.w != Size || p.h != Size {
|
||||
t.Fatalf("bad card dimensions: %d×%d", p.w, p.h)
|
||||
}
|
||||
colors := map[[4]byte]bool{}
|
||||
for i := 0; i < len(p.buf); i += 4 {
|
||||
colors[[4]byte(p.buf[i:i+4])] = true
|
||||
}
|
||||
if len(colors) < 4 {
|
||||
t.Fatalf("code preview has no syntax strokes: %d colors", len(colors))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ type Preset struct {
|
||||
// Explicit prescribes a default layout, without affecting existing saved layouts.
|
||||
Explicit bool
|
||||
Front []string
|
||||
// Splits place default panels beside their anchors; saved layouts are unchanged.
|
||||
Splits []PanelSplit
|
||||
// Rev drops layouts saved for an older revision of a built-in preset.
|
||||
Rev int
|
||||
Owner string
|
||||
@@ -40,6 +42,12 @@ type Preset struct {
|
||||
Base string
|
||||
}
|
||||
|
||||
type PanelSplit struct {
|
||||
Panel, Anchor string
|
||||
Side dock.Side
|
||||
Ratio float32
|
||||
}
|
||||
|
||||
type layout struct {
|
||||
tree []byte
|
||||
toolbar bool
|
||||
@@ -76,6 +84,7 @@ func (w *Workspace) RegisterPreset(p Preset) error {
|
||||
}
|
||||
p.Slots = maps.Clone(p.Slots)
|
||||
p.Front = slices.Clone(p.Front)
|
||||
p.Splits = slices.Clone(p.Splits)
|
||||
w.presets = append(w.presets, p)
|
||||
slices.SortStableFunc(w.presets, func(a, b Preset) int { return a.Order - b.Order })
|
||||
if w.cur == "" {
|
||||
@@ -157,6 +166,17 @@ func (w *Workspace) layoutFor(p *Preset) *dock.Tree {
|
||||
}
|
||||
t := dock.Columns(cols[SlotLeft], cols[SlotBrowser], cols[SlotCenter], cols[SlotBottom], cols[SlotRight])
|
||||
if p != nil {
|
||||
for _, split := range p.Splits {
|
||||
if !t.Has(split.Panel) || !t.Has(split.Anchor) || split.Panel == split.Anchor {
|
||||
continue
|
||||
}
|
||||
t.DockPanel(split.Panel, t.FindPanel(split.Anchor), split.Side)
|
||||
if node, ok := t.Node(t.FindPanel(split.Panel)); ok && split.Side != dock.SideCenter && node.Parent != 0 && split.Ratio > 0 && split.Ratio < 1 {
|
||||
if parent, ok := t.Node(node.Parent); ok && parent.Kind == dock.KindSplit {
|
||||
parent.Ratio = split.Ratio
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, id := range p.Front {
|
||||
t.Activate(id)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,16 @@ type FieldVisibility interface {
|
||||
FieldVisible(ctx *context.Editor, nodes []ids.NodeID, field *schema.FieldDescriptor) bool
|
||||
}
|
||||
|
||||
// FieldValidation supplies a localized explanation for an invalid field.
|
||||
type FieldValidation interface {
|
||||
FieldError(ctx *context.Editor, nodes []ids.NodeID, field *schema.FieldDescriptor) string
|
||||
}
|
||||
|
||||
// NormalizedCurve marks curves whose key times must stay within 0..1.
|
||||
type NormalizedCurve interface {
|
||||
NormalizedCurve(ctx *context.Editor, nodes []ids.NodeID, field *schema.FieldDescriptor) bool
|
||||
}
|
||||
|
||||
type CustomFunc func(ctx *context.Editor, nodes []ids.NodeID, defaultFields func())
|
||||
|
||||
func (f CustomFunc) Draw(ctx *context.Editor, nodes []ids.NodeID, defaultFields func()) {
|
||||
@@ -63,6 +73,28 @@ func (r *Registry) FieldVisible(t schema.TypeID, nodes []ids.NodeID, field *sche
|
||||
return !ok || filter.FieldVisible(r.editor, nodes, field)
|
||||
}
|
||||
|
||||
func (r *Registry) FieldError(t schema.TypeID, nodes []ids.NodeID, field *schema.FieldDescriptor) string {
|
||||
c, ok := r.Custom(t)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
if validation, ok := c.(FieldValidation); ok {
|
||||
return validation.FieldError(r.editor, nodes, field)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (r *Registry) NormalizedCurve(t schema.TypeID, nodes []ids.NodeID, field *schema.FieldDescriptor) bool {
|
||||
c, ok := r.Custom(t)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if limits, ok := c.(NormalizedCurve); ok {
|
||||
return limits.NormalizedCurve(r.editor, nodes, field)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{
|
||||
custom: map[schema.TypeID]Custom{},
|
||||
|
||||
@@ -90,6 +90,32 @@ func (s *safeCustom) FieldVisible(ctx *context.Editor, nodes []ids.NodeID, field
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *safeCustom) FieldError(ctx *context.Editor, nodes []ids.NodeID, field *schema.FieldDescriptor) (message string) {
|
||||
defer func() {
|
||||
if rec := recover(); rec != nil {
|
||||
s.reg.Disable(s.ext, fmt.Sprint(rec))
|
||||
message = ""
|
||||
}
|
||||
}()
|
||||
if v, ok := s.inner.(inspect.FieldValidation); ok {
|
||||
return v.FieldError(ctx, nodes, field)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *safeCustom) NormalizedCurve(ctx *context.Editor, nodes []ids.NodeID, field *schema.FieldDescriptor) (normalized bool) {
|
||||
defer func() {
|
||||
if rec := recover(); rec != nil {
|
||||
s.reg.Disable(s.ext, fmt.Sprint(rec))
|
||||
normalized = false
|
||||
}
|
||||
}()
|
||||
if v, ok := s.inner.(inspect.NormalizedCurve); ok {
|
||||
return v.NormalizedCurve(ctx, nodes, field)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type safeGizmo struct {
|
||||
inner gizmo.Gizmo
|
||||
ext string
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"aego/core/mathx"
|
||||
"aego/editor/core/follow"
|
||||
"aego/editor/core/presence"
|
||||
edselection "aego/editor/core/selection"
|
||||
"aego/editor/core/services"
|
||||
"aego/platform/input"
|
||||
"aego/render/text"
|
||||
"aego/ui/core"
|
||||
"aego/ui/theme"
|
||||
)
|
||||
|
||||
type toolbarTestHost struct {
|
||||
*providerTestHost
|
||||
auto bool
|
||||
}
|
||||
|
||||
func (h *toolbarTestHost) AutoApply() bool { return h.auto }
|
||||
func (h *toolbarTestHost) SetAutoApply(v bool) { h.auto = v }
|
||||
|
||||
func TestAgentHeaderControlsRemainUsableWhenNarrow(t *testing.T) {
|
||||
for _, width := range []float32{420, 260} {
|
||||
t.Run(strconv.Itoa(int(width)), func(t *testing.T) {
|
||||
h := &toolbarTestHost{providerTestHost: newProviderTestHost()}
|
||||
p := providerPanel(t, h.providerTestHost)
|
||||
p.d.Host = h
|
||||
p.d.Follow = follow.New(presence.New(), edselection.New(), nil)
|
||||
ui := core.New(text.NewFont(14, 1, 18, 14))
|
||||
ui.Theme = theme.Editor()
|
||||
in := &input.State{}
|
||||
r := mathx.Rect{W: width, H: 520}
|
||||
frame := func(at mathx.Vec2, down, up bool) {
|
||||
in.BeginFrame()
|
||||
in.ApplyMouseMove(at, mathx.Vec2{})
|
||||
if down {
|
||||
in.ApplyMouseDown(at, input.MouseLeft)
|
||||
}
|
||||
if up {
|
||||
in.ApplyMouseUp(at, input.MouseLeft)
|
||||
}
|
||||
ui.Begin(in, mathx.V2(800, 600), 1)
|
||||
ui.Region(r)
|
||||
p.Draw(&services.Frame{UI: ui}, r)
|
||||
ui.PopCursor()
|
||||
ui.End()
|
||||
}
|
||||
click := func(at mathx.Vec2) { frame(at, true, false); frame(at, false, true); frame(at, false, false) }
|
||||
frame(mathx.V2(0, 0), false, false)
|
||||
pad, row := ui.Theme.Padding, ui.Theme.RowHeight
|
||||
y := row + pad + pad + row/2
|
||||
more := mathx.V2(width-pad-row/2, y)
|
||||
followAt := mathx.V2(width-pad-row-4-row/2, y)
|
||||
right := width - pad - 2*(row+4)
|
||||
auto := mathx.V2(right-row/2, y)
|
||||
click(auto)
|
||||
if !h.auto {
|
||||
t.Fatal("auto-apply control is missing or overlaps its neighbours")
|
||||
}
|
||||
click(followAt)
|
||||
if !p.d.Follow.On() {
|
||||
t.Fatal("follow button is missing or overlaps its neighbours")
|
||||
}
|
||||
p.d.Follow.UserActive(1)
|
||||
click(followAt)
|
||||
if !p.d.Follow.Active() {
|
||||
t.Fatal("the follow button must resume a paused follow")
|
||||
}
|
||||
click(more)
|
||||
if !ui.PopupOpen(p.menu) {
|
||||
t.Fatal("session menu button is missing or overlaps its neighbours")
|
||||
}
|
||||
ui.CloseAllPopups()
|
||||
settings := mathx.V2(p.settingsRect.X+p.settingsRect.W/2, p.settingsRect.Y+p.settingsRect.H/2)
|
||||
click(settings)
|
||||
if !ui.PopupOpen(p.providerSettings) {
|
||||
t.Fatal("settings button is missing or overlaps the provider picker")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"aego/core/mathx"
|
||||
"aego/editor/core/document"
|
||||
"aego/editor/core/services"
|
||||
"aego/engine/base"
|
||||
"aego/platform/input"
|
||||
"aego/scene"
|
||||
"aego/ui/core"
|
||||
)
|
||||
|
||||
func TestMentionTypedSearchPickedAndInsertedAtCaret(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name, query, label, full string
|
||||
}{
|
||||
{"node", "Hero", "@Hero", "@Hero (#"},
|
||||
{"asset", "walk", "@player_walk.aeanim", "@assets/anim/player_walk.aeanim"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
types, err := base.NewTypes()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sc := scene.New(types, nil, scene.Options{})
|
||||
sc.Create(sc.Root(), "Hero")
|
||||
sc.Flush()
|
||||
mgr := document.NewManager(document.Options{Root: t.TempDir(), Types: types})
|
||||
doc := mgr.New("scenes/main.aescene", sc)
|
||||
host := newProviderTestHost()
|
||||
p := providerPanel(t, host)
|
||||
p.d.Docs = &quickDocs{doc: doc}
|
||||
p.d.Assets = func() []string { return []string{"assets/anim/player_walk.aeanim", "assets/anim/enemy.aectrl"} }
|
||||
p.Ask("end")
|
||||
ui, in := core.New(nil), &input.State{}
|
||||
r := mathx.Rect{W: 480, H: 520}
|
||||
draw := func() {
|
||||
ui.Begin(in, mathx.V2(800, 600), 1)
|
||||
ui.Region(r)
|
||||
p.Draw(&services.Frame{UI: ui}, r)
|
||||
ui.PopCursor()
|
||||
ui.End()
|
||||
}
|
||||
frame := func(event func()) {
|
||||
in.BeginFrame()
|
||||
if event != nil {
|
||||
event()
|
||||
}
|
||||
draw()
|
||||
}
|
||||
frame(nil)
|
||||
frame(func() { in.ApplyKeyDown(input.KeyHome, 0, 0, false) })
|
||||
frame(func() { in.ApplyKeyUp(input.KeyHome, 0, 0) })
|
||||
frame(func() { in.ApplyText([]byte("@")) })
|
||||
if p.pal == nil || !p.pal.Active() || p.input != "end" || ui.Focus() != p.pal.SearchID() {
|
||||
t.Fatalf("opening lost the draft or focus: input=%q focus=%v", p.input, ui.Focus())
|
||||
}
|
||||
frame(func() { in.ApplyText([]byte(tc.query)) })
|
||||
if p.pal.Query() != tc.query || p.input != "end" {
|
||||
t.Fatalf("search copied trigger or leaked into draft: query=%q input=%q", p.pal.Query(), p.input)
|
||||
}
|
||||
frame(func() { in.ApplyKeyDown(input.KeyEnter, 0, 0, false) })
|
||||
if p.pal.Active() || p.input != tc.label+" end" || len(p.picked) != 1 {
|
||||
t.Fatalf("selection not inserted at caret: %q, %#v", p.input, p.picked)
|
||||
}
|
||||
frame(func() { in.ApplyKeyUp(input.KeyEnter, 0, 0) })
|
||||
frame(func() { in.ApplyText([]byte("!")) })
|
||||
if p.input != tc.label+" !end" {
|
||||
t.Fatalf("typing did not resume at the caret: %q", p.input)
|
||||
}
|
||||
p.send()
|
||||
if len(host.sent) != 1 || !strings.Contains(host.sent[0], tc.full) {
|
||||
t.Fatalf("missing full reference in sent text: %q", host.sent)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMentionAssetIcons(t *testing.T) {
|
||||
for path, want := range map[string]string{
|
||||
"assets/walk.aeanim": "anim.key", "assets/player.aectrl": "anim.graph",
|
||||
"assets/scene.aescene": "file.scene", "assets/data.aetable": "file.table",
|
||||
} {
|
||||
if got := assetIcon(path); got != want {
|
||||
t.Errorf("assetIcon(%q) = %q, want %q", path, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
+133
-63
File diff suppressed because it is too large
Load Diff
@@ -170,7 +170,7 @@ func (p *Panel) providerRow(ui *core.Context, r mathx.Rect) float32 {
|
||||
row := mathx.Rect{X: r.X + th.Padding, Y: r.Y + th.Padding/2, W: max(0, r.W-2*th.Padding), H: th.RowHeight}
|
||||
ui.Rect(mathx.Rect{X: r.X, Y: r.Y, W: r.W, H: height}, th.Header)
|
||||
label := i18n.Tr("editor.agent.provider_settings", "Agent settings")
|
||||
settingsW := min(row.W/2, ui.TextSize(label).X+20)
|
||||
settingsW := min(row.W, row.H)
|
||||
choice := mathx.Rect{X: row.X, Y: row.Y, W: min(140, max(0, row.W-settingsW-6)), H: row.H}
|
||||
index := 0
|
||||
if provider == "codex" {
|
||||
@@ -187,7 +187,7 @@ func (p *Panel) providerRow(ui *core.Context, r mathx.Rect) float32 {
|
||||
}
|
||||
settings := mathx.Rect{X: row.X + row.W - settingsW, Y: row.Y, W: settingsW, H: row.H}
|
||||
p.settingsRect = settings
|
||||
if widgets.ButtonRect(ui, ui.ID("provider-settings"), settings, label) {
|
||||
if agentButton(ui, ui.ID("provider-settings"), settings, "panel.settings", "", label, ui.PopupOpen(p.providerSettings)) {
|
||||
p.readProviderSettings()
|
||||
p.loadModels()
|
||||
ui.OpenPopup(p.providerSettings, mathx.Vec2{X: max(0, settings.X+settings.W-360), Y: row.Y + row.H + 4})
|
||||
@@ -202,6 +202,8 @@ func (p *Panel) providerRow(ui *core.Context, r mathx.Rect) float32 {
|
||||
}
|
||||
mr := mathx.Rect{X: choice.X + choice.W + 8, Y: row.Y, W: max(0, settings.X-choice.X-choice.W-16), H: row.H}
|
||||
ui.TextIn(mr, p.fits[4].get(ui, model, mr.W), th.TextDim, core.AlignLeft)
|
||||
ui.MouseOver(ui.ID("model-status"), mr)
|
||||
widgets.Tooltip(ui, ui.ID("model-status"), model)
|
||||
}
|
||||
p.providerPopup(ui)
|
||||
return height
|
||||
|
||||
@@ -6,12 +6,10 @@ import (
|
||||
|
||||
"aego/core/mathx"
|
||||
"aego/editor/core/agentui"
|
||||
"aego/editor/core/follow"
|
||||
"aego/editor/core/presence"
|
||||
"aego/engine/i18n"
|
||||
"aego/ui/core"
|
||||
"aego/ui/widgets"
|
||||
wpresence "aego/ui/widgets/presence"
|
||||
)
|
||||
|
||||
// dot is one tool call on the activity timeline: where its block is and how it went.
|
||||
@@ -165,49 +163,6 @@ func (p *Panel) undoBack(card int) {
|
||||
}
|
||||
}
|
||||
|
||||
// where is the row under the header: where the agent is now, and Resume while following is paused.
|
||||
func (p *Panel) where(ui *core.Context, r mathx.Rect) {
|
||||
th := &ui.Theme
|
||||
f := p.d.Follow
|
||||
if f == nil {
|
||||
return
|
||||
}
|
||||
ui.Rect(r, th.PanelAlt)
|
||||
x := r.X + th.Padding
|
||||
right := r.X + r.W - th.Padding
|
||||
if f.Paused() {
|
||||
label := i18n.Tr("editor.agent.resume", "Resume Following")
|
||||
bw := ui.TextSize(label).X + 20
|
||||
if widgets.ButtonRect(ui, ui.ID("resume"), mathx.Rect{X: right - bw, Y: r.Y + 2, W: bw, H: r.H - 4}, label) {
|
||||
f.ResumeNow()
|
||||
}
|
||||
right -= bw + 6
|
||||
}
|
||||
sp, at, ok := f.Model.Where(follow.Agent, p.now())
|
||||
if !ok {
|
||||
text := i18n.Tr("editor.agent.following", "Following the agent")
|
||||
if f.Paused() {
|
||||
text = i18n.Tr("editor.agent.following_paused", "Following paused while you work")
|
||||
}
|
||||
ui.Text(p.fits[3].get(ui, text, right-x), mathx.Vec2{X: x, Y: ui.TextY(r)}, th.TextDim)
|
||||
return
|
||||
}
|
||||
wpresence.Marker(ui, mathx.Rect{X: x, Y: r.Y + (r.H-14)/2, W: 14, H: 14}, p.now()-at)
|
||||
x += 20
|
||||
label := p.spotLabel(sp)
|
||||
lr := mathx.Rect{X: x, Y: r.Y, W: min(ui.TextSize(label).X+4, right-x), H: r.H}
|
||||
id := ui.ID("where")
|
||||
if ui.Clicked(id, lr) {
|
||||
f.GoTo(sp)
|
||||
}
|
||||
col := th.Text
|
||||
if ui.Hot() == id {
|
||||
col = th.Accent
|
||||
}
|
||||
ui.Text(p.fits[3].get(ui, label, right-x), mathx.Vec2{X: x, Y: ui.TextY(r)}, col)
|
||||
widgets.Tooltip(ui, id, i18n.Tr("editor.agent.where_tip", "Where the agent works now; click to go there"))
|
||||
}
|
||||
|
||||
// spotLabel is a place as the user names it: the file, then the node and the component, or the line.
|
||||
func (p *Panel) spotLabel(sp presence.Spot) string {
|
||||
label := docName(sp.Doc)
|
||||
|
||||
@@ -441,7 +441,11 @@ func (p *Panel) empty(ui *core.Context, body mathx.Rect) {
|
||||
ui.TextIn(mathx.Rect{X: x, Y: y, W: w, H: th.RowHeight}, i18n.Tr("editor.agent.empty_title", "Ask the agent"), th.Text, core.AlignCenter)
|
||||
y += th.RowHeight
|
||||
hint := i18n.Tr("editor.agent.empty_hint", "Ask about the project or describe a change. Type @ to mention a node or an asset. Changes wait for you to apply them.")
|
||||
for _, line := range widgets.WrapLines(ui, hint, w) {
|
||||
if p.emptyHint != hint || p.emptyHintWidth != w || p.emptyHintFont != ui.Font() {
|
||||
p.emptyHint, p.emptyHintWidth, p.emptyHintFont = hint, w, ui.Font()
|
||||
p.emptyHintLines = widgets.WrapLines(ui, hint, w)
|
||||
}
|
||||
for _, line := range p.emptyHintLines {
|
||||
ui.TextIn(mathx.Rect{X: x, Y: y, W: w, H: ui.LineHeight()}, line, th.TextDim, core.AlignCenter)
|
||||
y += ui.LineHeight()
|
||||
}
|
||||
@@ -477,7 +481,7 @@ func (p *Panel) empty(ui *core.Context, body mathx.Rect) {
|
||||
}
|
||||
ui.RoundRect(er, th.Radius.Button, fill)
|
||||
widgets.IconAt(ui, mathx.Rect{X: er.X + 8, Y: er.Y + (er.H-14)/2, W: 14, H: 14}, "agent.new", th.TextDim)
|
||||
ui.Text(widgets.Elide(ui, ex, er.W-36), mathx.Vec2{X: er.X + 30, Y: ui.TextY(er)}, th.Text)
|
||||
ui.Text(p.emptyFits[i].get(ui, ex, er.W-36), mathx.Vec2{X: er.X + 30, Y: ui.TextY(er)}, th.Text)
|
||||
y += er.H + th.Spacing
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,6 +234,7 @@ func (p *Panel) refresh() {
|
||||
if changed {
|
||||
p.items = append(pl.Assets(), extras...)
|
||||
p.addCells()
|
||||
p.stacks = nil
|
||||
p.prune()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ func (p *Panel) stack(dir string) []int {
|
||||
var out []int
|
||||
for i := range p.items {
|
||||
it := &p.items[i]
|
||||
if path.Dir(it.Path) != dir || it.Kind == asset.ArtScript || it.Kind == asset.ArtData || it.Kind == asset.ArtTable {
|
||||
if path.Dir(it.Path) != dir || p.isCell(it.ID) {
|
||||
continue
|
||||
}
|
||||
out = append(out, i)
|
||||
@@ -212,12 +212,16 @@ func (p *Panel) wave(a *pipeline.AssetInfo, r mathx.Rect) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// folderStack draws up to three previews as sheets sticking out of the folder icon.
|
||||
// folderStack draws scalable folder shapes and up to three sheets of contents.
|
||||
func (p *Panel) folderStack(dir string, img mathx.Rect) {
|
||||
ui := p.f.UI
|
||||
th := &ui.Theme
|
||||
icon := img.W * 0.82
|
||||
at := mathx.Rect{X: img.X + (img.W-icon)/2, Y: img.Y + (img.H-icon)/2 + icon*0.06, W: icon, H: icon}
|
||||
back := th.Accent
|
||||
back.R, back.G, back.B = back.R*0.7, back.G*0.7, back.B*0.7
|
||||
ui.RoundRect(mathx.Rect{X: at.X + icon*0.07, Y: at.Y + icon*0.12, W: icon * 0.36, H: icon * 0.22}, icon*0.035, back)
|
||||
ui.RoundRect(mathx.Rect{X: at.X + icon*0.07, Y: at.Y + icon*0.22, W: icon * 0.86, H: icon * 0.64}, icon*0.035, back)
|
||||
list := p.stack(dir)
|
||||
n := min(len(list), 3)
|
||||
if img.W < 40 {
|
||||
@@ -238,7 +242,9 @@ func (p *Panel) folderStack(dir string, img mathx.Rect) {
|
||||
}
|
||||
ui.Outline(cr, th.Border)
|
||||
}
|
||||
widgets.IconAt(ui, at, "file.folder", th.Accent)
|
||||
front := mathx.Rect{X: at.X + icon*0.07, Y: at.Y + icon*0.43, W: icon * 0.86, H: icon * 0.43}
|
||||
ui.RoundRect(front, icon*0.035, th.Accent)
|
||||
ui.Rect(mathx.Rect{X: front.X, Y: front.Y, W: front.W, H: max(1, icon*0.025)}, back)
|
||||
}
|
||||
|
||||
func (p *Panel) code(lines []string, r mathx.Rect) {
|
||||
|
||||
@@ -3,6 +3,9 @@ package assets
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"aego/asset"
|
||||
"aego/asset/pipeline"
|
||||
"aego/core/ids"
|
||||
"aego/editor/core/code/highlight"
|
||||
)
|
||||
|
||||
@@ -26,3 +29,19 @@ func TestDeclName(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFolderStackIncludesScriptsAndData(t *testing.T) {
|
||||
p := New(Deps{})
|
||||
p.items = []pipeline.AssetInfo{
|
||||
{ID: ids.AssetID{Lo: 1}, Path: "scripts/player/player.aes", Kind: asset.ArtScript},
|
||||
{ID: ids.AssetID{Lo: 2}, Path: "scripts/other.aes", Kind: asset.ArtScript},
|
||||
{ID: ids.AssetID{Lo: 3}, Path: "assets/data/settings.aedata", Kind: asset.ArtData},
|
||||
{ID: ids.AssetID{Lo: 4}, Path: "assets/data/values.aetable", Kind: asset.ArtTable},
|
||||
}
|
||||
if got := p.stack("scripts/player"); len(got) != 1 || got[0] != 0 {
|
||||
t.Fatalf("script previews = %v", got)
|
||||
}
|
||||
if got := p.stack("assets/data"); len(got) != 2 || got[0] != 2 || got[1] != 3 {
|
||||
t.Fatalf("data previews = %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,7 +347,7 @@ func (p *Panel) settings(ui *uicore.Context) {
|
||||
done := p.rep != nil && !p.rep.Blocked && p.rep.Out != ""
|
||||
bar := ui.Cursor().Next(th.RowHeight + 6)
|
||||
bx := bar.X + labelW
|
||||
if p.action(ui, "export", &bx, bar.Y, bar.H, "file.package", i18n.Tr("editor.export.action.export", "Export"), !busy, true) {
|
||||
if p.action(ui, "export", &bx, bar.Y, bar.H, "file.export", i18n.Tr("editor.export.action.export", "Export"), !busy, true) {
|
||||
p.export(out)
|
||||
}
|
||||
if p.action(ui, "check", &bx, bar.Y, bar.H, "ui.check", i18n.Tr("editor.export.action.check", "Check"), !busy, false) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -154,6 +154,18 @@ func (p *Panel) fieldIn(d *document.SceneDocument, td *schema.TypeDescriptor, fd
|
||||
}
|
||||
}
|
||||
tip := schema.FieldTip(td, fd)
|
||||
fieldError := p.reg.FieldError(td.ID, p.nodes, fd)
|
||||
if fieldError != "" {
|
||||
tip += "\n" + fieldError
|
||||
}
|
||||
if fd.Kind == schema.KindCurve {
|
||||
st := p.curves[key]
|
||||
if st == nil {
|
||||
st = widgets.NewCurveState()
|
||||
p.curves[key] = st
|
||||
}
|
||||
st.SetNormalizedTime(p.reg.NormalizedCurve(td.ID, p.nodes, fd))
|
||||
}
|
||||
if mixed {
|
||||
tip = p.mixedTip(d, td, fd, tip)
|
||||
}
|
||||
@@ -183,6 +195,9 @@ func (p *Panel) fieldIn(d *document.SceneDocument, td *schema.TypeDescriptor, fd
|
||||
wpresence.Light(ui, row, m.FieldLight(d.Path, p.nodes[0], td.FullName, fd.Name, p.f.Now))
|
||||
editing = m.Editing(d.Path, p.nodes[0], td.FullName, fd.Name)
|
||||
}
|
||||
if fieldError != "" {
|
||||
ui.List().RectOutline(row, ui.Theme.Error, 1)
|
||||
}
|
||||
switch {
|
||||
case editing:
|
||||
ui.Rect(mathx.Rect{X: row.X - 5, Y: row.Y + 1, W: 2, H: max(row.H-2, 2)}, ui.Theme.Agent.Presence)
|
||||
@@ -452,7 +467,7 @@ func (p *Panel) scalar(d *document.SceneDocument, id core.ID, r mathx.Rect, fd *
|
||||
st = widgets.NewCurveState()
|
||||
p.curves[id] = st
|
||||
}
|
||||
cv := v.Cv
|
||||
cv := v.Cv.Clone()
|
||||
e := widgets.CurveField(ui, id, r, &cv, mixed, st)
|
||||
if e.Changed {
|
||||
*v = schema.Curve(cv)
|
||||
|
||||
@@ -47,7 +47,7 @@ func (p *Panel) input3(d *document.SceneDocument, r mathx.Rect) {
|
||||
}
|
||||
ui := p.ui()
|
||||
id := ui.ID("view3")
|
||||
over := ui.MouseOver(id, r) && !p.barRect.Contains(ui.Mouse())
|
||||
over := !p.barRect.Contains(ui.Mouse()) && ui.MouseOver(id, r)
|
||||
mods := ui.Mods()
|
||||
in := Input3{
|
||||
Mouse: p.toPixels(ui.Mouse()),
|
||||
|
||||
@@ -440,8 +440,8 @@ func (p *Panel) input(d *document.SceneDocument, r mathx.Rect) {
|
||||
ui := p.ui()
|
||||
id := ui.ID("view")
|
||||
cam := &d.View.Camera
|
||||
over := ui.MouseOver(id, r)
|
||||
onBar := p.barRect.Contains(ui.Mouse())
|
||||
over := !onBar && ui.MouseOver(id, r)
|
||||
if over && !onBar && p.mode == modeIdle && (ui.MousePressed(input.MouseMiddle) || ui.MousePressed(input.MouseRight)) {
|
||||
p.mode = modePan
|
||||
p.panRight = ui.MousePressed(input.MouseRight) && !ui.MousePressed(input.MouseMiddle)
|
||||
@@ -575,10 +575,10 @@ func (p *Panel) toolbar(d *document.SceneDocument, r mathx.Rect) {
|
||||
icon string
|
||||
tip string
|
||||
}{
|
||||
{"tool.select", i18n.Tr("editor.sceneview.tool.select", "Select (Q)")},
|
||||
{"tool.move", i18n.Tr("editor.sceneview.tool.move", "Move (W)")},
|
||||
{"tool.rotate", i18n.Tr("editor.sceneview.tool.rotate", "Rotate (E)")},
|
||||
{"tool.scale", i18n.Tr("editor.sceneview.tool.scale", "Scale (R)")},
|
||||
{"tool.select", i18n.Tr("editor.sceneview.tool.select", "Select nodes in the scene (Q)")},
|
||||
{"tool.move", i18n.Tr("editor.sceneview.tool.move", "Move selected nodes (W)")},
|
||||
{"tool.rotate", i18n.Tr("editor.sceneview.tool.rotate", "Rotate selected nodes (E)")},
|
||||
{"tool.scale", i18n.Tr("editor.sceneview.tool.scale", "Scale selected nodes (R)")},
|
||||
}
|
||||
for i, t := range tools {
|
||||
if tb.Button(ui.IDInt(300+i), t.icon, t.tip, p.d.Tools.Tool == uint8(i), true) {
|
||||
@@ -586,13 +586,13 @@ func (p *Panel) toolbar(d *document.SceneDocument, r mathx.Rect) {
|
||||
}
|
||||
}
|
||||
tb.Separator()
|
||||
if tb.Button(ui.ID("grid"), "view.grid", i18n.Tr("editor.sceneview.grid", "Grid"), p.d.Tools.Grid, true) {
|
||||
if tb.Button(ui.ID("grid"), "view.grid", i18n.Tr("editor.sceneview.grid", "Show or hide the scene grid"), p.d.Tools.Grid, true) {
|
||||
p.d.Tools.Grid = !p.d.Tools.Grid
|
||||
}
|
||||
if tb.Button(ui.ID("snap"), "view.snap", i18n.Tr("editor.sceneview.snap_hold_ctrl_to_invert", "Snap (hold Ctrl to invert)"), p.d.Tools.Snap, true) {
|
||||
if tb.Button(ui.ID("snap"), "view.snap", i18n.Tr("editor.sceneview.snap_hold_ctrl_to_invert", "Snap transforms to steps (hold Ctrl to invert)"), p.d.Tools.Snap, true) {
|
||||
p.d.Tools.Snap = !p.d.Tools.Snap
|
||||
}
|
||||
if tb.Button(ui.ID("vectors"), "view.vectors", i18n.Tr("editor.sceneview.vectors_v", "Vectors (V)"), p.d.Viewport.Overlay.On(vectors.CommandToggle), true) {
|
||||
if tb.Button(ui.ID("vectors"), "view.vectors", i18n.Tr("editor.sceneview.vectors_v", "Show or hide debug vectors (V)"), p.d.Viewport.Overlay.On(vectors.CommandToggle), true) {
|
||||
p.d.Viewport.Overlay.Toggle(vectors.CommandToggle)
|
||||
}
|
||||
for _, cmd := range extra {
|
||||
@@ -615,7 +615,7 @@ func (p *Panel) toolbar(d *document.SceneDocument, r mathx.Rect) {
|
||||
}
|
||||
zid := ui.ID("zoom")
|
||||
if !three {
|
||||
if clicked, zr := tb.TextButton(zid, zoom, i18n.Tr("editor.sceneview.zoom", "Zoom")); clicked {
|
||||
if clicked, zr := tb.TextButton(zid, zoom, i18n.Tr("editor.sceneview.zoom", "Choose the scene view zoom")); clicked {
|
||||
if ui.PopupOpen(zid) {
|
||||
ui.ClosePopup(zid)
|
||||
} else {
|
||||
@@ -627,7 +627,7 @@ func (p *Panel) toolbar(d *document.SceneDocument, r mathx.Rect) {
|
||||
tb.Separator()
|
||||
cid := ui.ID("create")
|
||||
createX := tb.X()
|
||||
if tb.Button(cid, "ui.plus", i18n.Tr("editor.sceneview.create_button", "Create"), false, true) {
|
||||
if tb.Button(cid, "ui.plus", i18n.Tr("editor.sceneview.create_button", "Create a node in the scene"), false, true) {
|
||||
anchor := mathx.Vec2{X: createX, Y: bar.Y + bar.H}
|
||||
if three {
|
||||
p.createAt3(p.c3.Cam.Target, &anchor)
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package sceneview
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"aego/core/mathx"
|
||||
"aego/editor/core/context"
|
||||
"aego/editor/core/document"
|
||||
"aego/editor/core/services"
|
||||
"aego/platform/input"
|
||||
"aego/render/text"
|
||||
"aego/scene"
|
||||
"aego/ui/core"
|
||||
"aego/ui/theme"
|
||||
)
|
||||
|
||||
func TestToolbarHoverSurvivesSceneInput(t *testing.T) {
|
||||
for _, mode := range []string{"2D", "3D"} {
|
||||
t.Run(mode, func(t *testing.T) {
|
||||
types := testTypes(t)
|
||||
s := scene.New(types, nil, scene.Options{})
|
||||
mgr := document.NewManager(document.Options{Root: t.TempDir(), Types: types})
|
||||
doc := mgr.New("test.aescene", s)
|
||||
ed := &context.Editor{Docs: mgr}
|
||||
ui := core.New(text.NewFont(14, 1, 18, 14))
|
||||
ui.Theme = theme.Editor()
|
||||
p := New(nil, Deps{Docs: ed, Tools: &context.SceneSettings{}, Viewport: &services.Viewport{Overlay: &context.Overlays{}}})
|
||||
p.f = &services.Frame{UI: ui}
|
||||
p.cur = &world{scene: s}
|
||||
p.scale = 1
|
||||
if mode == "3D" {
|
||||
p.viewMode = Mode3D
|
||||
}
|
||||
in := &input.State{}
|
||||
for i := 0; i < 3; i++ {
|
||||
in.BeginFrame()
|
||||
in.ApplyMouseMove(mathx.V2(18, 18), mathx.Vec2{})
|
||||
ui.Time = float64(i) * 0.4
|
||||
ui.Begin(in, mathx.V2(800, 600), 1)
|
||||
r := mathx.Rect{W: 800, H: 600}
|
||||
p.toolbar(doc, r)
|
||||
if mode == "3D" {
|
||||
p.input3(doc, r)
|
||||
} else {
|
||||
p.input(doc, r)
|
||||
}
|
||||
id := ui.IDInt(300)
|
||||
if ui.Hot() != id {
|
||||
t.Fatalf("scene input stole toolbar hover: got %v, want %v", ui.Hot(), id)
|
||||
}
|
||||
if i == 2 && ui.HoverDuration(id) < 0.5 {
|
||||
t.Fatal("hover never lasted long enough to show the tooltip")
|
||||
}
|
||||
ui.End()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -326,7 +326,7 @@ func (s *Service) scripts() {
|
||||
short, pkg = full[i+1:], strings.ReplaceAll(full[:i], ".", "/")
|
||||
}
|
||||
s.entries = append(s.entries, e)
|
||||
s.items = append(s.items, palette.Item{Key: "script:" + full, Label: inspect.Humanize(short), Category: scriptsCat, Detail: pkg, Icon: "file.script", Disabled: why})
|
||||
s.items = append(s.items, palette.Item{Key: "script:" + full, Label: inspect.Humanize(short), Category: scriptsCat, Detail: pkg, Icon: "comp.script", Disabled: why})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -589,7 +589,7 @@ var categoryIcons = map[string]string{
|
||||
"2D": "cat.rendering",
|
||||
"3D": "cat.3d",
|
||||
"Camera": "cat.camera",
|
||||
"Lights": "view.overlays",
|
||||
"Lights": "cat.render2d",
|
||||
"Physics": "comp.rigidbody",
|
||||
"Audio": "cat.audio",
|
||||
"Animation": "anim.graph",
|
||||
|
||||
+20
-2
@@ -104,7 +104,11 @@ func (a *App) commandItems(cat string) {
|
||||
widgets.Separator(ui)
|
||||
}
|
||||
last = d.Order
|
||||
if widgets.Entry(ui, widgets.Item{Label: d.Title(), Shortcut: reg.Shortcut(d.ID), Icon: d.Icon, Tip: d.TipText(), Enabled: reg.Enabled(d.ID), Checked: d.Checked != nil && d.Checked()}) {
|
||||
if cat == "Help" && d.ID == CommandReportBug {
|
||||
a.reportBugMenu(d.Title())
|
||||
continue
|
||||
}
|
||||
if widgets.Entry(ui, widgets.Item{Label: d.Title(), Shortcut: reg.Shortcut(d.ID), Icon: d.Icon, Tip: d.TipText(), Enabled: reg.Enabled(d.ID), Checked: d.Checked != nil && d.Checked(), KeepOpen: d.Checked != nil}) {
|
||||
reg.Run(d.ID)
|
||||
}
|
||||
}
|
||||
@@ -114,6 +118,20 @@ func (a *App) commandItems(cat string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) reportBugMenu(title string) {
|
||||
ui := a.ui
|
||||
if !widgets.SubMenu(ui, title, 300) {
|
||||
return
|
||||
}
|
||||
if widgets.MenuItem(ui, i18n.Tr("editor.shell.report_issue", "Create an issue…"), "", true, false) {
|
||||
a.ctx.Commands.Run(CommandReportBug)
|
||||
}
|
||||
if widgets.MenuItem(ui, i18n.Tr("editor.shell.contact_telegram", "Telegram: @sanspie"), "", true, false) {
|
||||
a.openSupportURL(telegramURL)
|
||||
}
|
||||
widgets.EndSubMenu(ui)
|
||||
}
|
||||
|
||||
func (a *App) sceneDocs() {
|
||||
ui := a.ui
|
||||
docs := a.ctx.Docs.Docs()
|
||||
@@ -305,7 +323,7 @@ func (a *App) subMenu(cat string, sep bool) {
|
||||
return
|
||||
}
|
||||
for _, d := range reg.Category(cat) {
|
||||
if widgets.Entry(ui, widgets.Item{Label: subLabel(cat, d.Title()), Tip: d.TipText(), Enabled: true, Checked: d.Checked()}) {
|
||||
if widgets.Entry(ui, widgets.Item{Label: subLabel(cat, d.Title()), Tip: d.TipText(), Enabled: true, Checked: d.Checked(), KeepOpen: true}) {
|
||||
reg.Run(d.ID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ func (a *App) initExport() error {
|
||||
}
|
||||
a.exportP = exportpanel.New(exportpanel.Deps{Exporter: x, Tasks: a.tasks, PickFolder: a.pickFolder})
|
||||
return a.ctx.Commands.Register(command.Desc{
|
||||
ID: CommandExport, Name: "Export…", Category: "File", Order: 820, Icon: "file.package",
|
||||
ID: CommandExport, Name: "Export…", Category: "File", Order: 820, Icon: "file.export",
|
||||
Run: func() error {
|
||||
a.ws.Show(exportpanel.ID)
|
||||
return nil
|
||||
|
||||
@@ -2,7 +2,6 @@ package shell
|
||||
|
||||
import (
|
||||
"math"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"aego/core/mathx"
|
||||
@@ -14,7 +13,6 @@ import (
|
||||
edocs "aego/editor/panels/docs"
|
||||
"aego/editor/panels/help"
|
||||
"aego/editor/panels/welcome"
|
||||
"aego/engine"
|
||||
"aego/engine/docs"
|
||||
"aego/engine/i18n"
|
||||
"aego/scene"
|
||||
@@ -56,11 +54,7 @@ func (a *App) registerHelp() error {
|
||||
}},
|
||||
{ID: CommandAbout, Name: "About Aego", Category: "Help", Order: 900,
|
||||
Run: func() error {
|
||||
a.ctx.Ask(context.Question{
|
||||
Title: i18n.Tr("editor.shell.about_aego", "About Aego"),
|
||||
Message: i18n.Tr("editor.shell.about_version", "Aego engine {version}", i18n.Str("version", engine.Version)) + " · " + runtime.Version() + " · " + runtime.GOOS + "/" + runtime.GOARCH,
|
||||
Buttons: []string{i18n.Tr("editor.shell.close", "Close")},
|
||||
})
|
||||
a.push(modal{kind: modalAbout})
|
||||
return nil
|
||||
}},
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -36,3 +36,32 @@ func TestCreateKeepsChosenWorkspace(t *testing.T) {
|
||||
t.Fatalf("result = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultProjectNameSkipsExistingEntries(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
l := New(Options{Home: dir})
|
||||
if l.name != "new-game" {
|
||||
t.Fatalf("empty location: %q", l.name)
|
||||
}
|
||||
if err := os.Mkdir(filepath.Join(dir, "new-game"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "new-game-2"), nil, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := nextProjectName(dir); got != "new-game-3" {
|
||||
t.Fatalf("next available name = %q", got)
|
||||
}
|
||||
if _, err := projectTarget(dir, "new-game"); err == nil {
|
||||
t.Fatal("an existing project directory was accepted")
|
||||
}
|
||||
if _, err := projectTarget(dir, "new-game-2"); err == nil {
|
||||
t.Fatal("an existing file was accepted")
|
||||
}
|
||||
if _, err := projectTarget(dir, "../other"); err == nil {
|
||||
t.Fatal("a path was accepted as a project name")
|
||||
}
|
||||
if target, err := projectTarget(dir, "new-game-3"); err != nil || target != filepath.Join(dir, "new-game-3") {
|
||||
t.Fatalf("free target = %q, %v", target, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,14 @@ package shell
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"aego/core/mathx"
|
||||
"aego/editor/core/context"
|
||||
"aego/editor/core/document"
|
||||
"aego/engine"
|
||||
"aego/engine/i18n"
|
||||
"aego/platform/input"
|
||||
"aego/ui/core"
|
||||
@@ -24,6 +26,7 @@ const (
|
||||
modalClose
|
||||
modalLock
|
||||
modalAsk
|
||||
modalAbout
|
||||
)
|
||||
|
||||
type modal struct {
|
||||
@@ -167,6 +170,53 @@ func (a *App) drawModals() {
|
||||
md.ask.Then(c)
|
||||
}
|
||||
}
|
||||
case modalAbout:
|
||||
a.drawAbout(id)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) drawAbout(id core.ID) {
|
||||
ui, th := a.ui, &a.ui.Theme
|
||||
size := ui.Size()
|
||||
w := max(float32(1), min(float32(480), size.X-40))
|
||||
h := min(size.Y, 2*th.Padding+7*(th.RowHeight+th.Spacing))
|
||||
r := mathx.Rect{X: (size.X - w) / 2, Y: max(0, (size.Y-h)/3), W: w, H: h}
|
||||
if !ui.BeginModal(id, r) {
|
||||
return
|
||||
}
|
||||
widgets.Label(ui, i18n.Tr("editor.shell.about_aego", "About Aego"))
|
||||
widgets.LabelDim(ui, i18n.Tr("editor.shell.about_version", "Aego engine {version}", i18n.Str("version", engine.Version))+" · "+runtime.Version()+" · "+runtime.GOOS+"/"+runtime.GOARCH)
|
||||
ui.Cursor().Next(0)
|
||||
widgets.LabelDim(ui, i18n.Tr("editor.shell.created_by", "Created by: sanspie"))
|
||||
link := ui.Cursor().Next(0)
|
||||
link.W = min(link.W, ui.TextSize("about.akarpov.ru").X)
|
||||
linkID := ui.ID("about.developer")
|
||||
openDeveloper := ui.Clicked(linkID, link)
|
||||
color := th.Accent
|
||||
if ui.Hot() == linkID {
|
||||
ui.SetCursorShape(core.CursorPointer)
|
||||
ui.Rect(mathx.Rect{X: link.X, Y: link.Y + link.H - 2, W: link.W, H: 1}, color)
|
||||
}
|
||||
ui.Text("about.akarpov.ru", mathx.Vec2{X: link.X, Y: ui.TextY(link)}, color)
|
||||
widgets.LabelDim(ui, ":3")
|
||||
row := ui.Cursor().Next(0)
|
||||
closeLabel := i18n.Tr("editor.shell.close", "Close")
|
||||
websiteLabel := i18n.Tr("editor.shell.open_website", "Open website")
|
||||
closeW := ui.TextSize(closeLabel).X + 28
|
||||
websiteW := ui.TextSize(websiteLabel).X + 28
|
||||
closeRect := mathx.Rect{X: row.X + row.W - closeW, Y: row.Y, W: closeW, H: row.H}
|
||||
websiteRect := mathx.Rect{X: closeRect.X - websiteW - 6, Y: row.Y, W: websiteW, H: row.H}
|
||||
openRepo := widgets.ButtonRect(ui, ui.ID("about.repository"), websiteRect, websiteLabel)
|
||||
close := widgets.ButtonRect(ui, ui.ID("about.close"), closeRect, closeLabel) || ui.KeyPressed(input.KeyEscape)
|
||||
ui.EndModal()
|
||||
if openDeveloper {
|
||||
a.openSupportURL(developerURL)
|
||||
}
|
||||
if openRepo {
|
||||
a.openSupportURL(repositoryURL)
|
||||
}
|
||||
if close {
|
||||
a.pop()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -82,6 +82,13 @@ func (a *App) buildNativeMenu() ([]platform.Menu, []func()) {
|
||||
}
|
||||
last = d.Order
|
||||
id := d.ID
|
||||
if cat == "Help" && id == CommandReportBug {
|
||||
items = append(items, platform.MenuItem{Label: d.Title(), Enabled: true, Items: []platform.MenuItem{
|
||||
{ID: act(func() { reg.Run(CommandReportBug) }), Label: i18n.Tr("editor.shell.report_issue", "Create an issue…"), Enabled: true},
|
||||
{ID: act(func() { a.openSupportURL(telegramURL) }), Label: i18n.Tr("editor.shell.contact_telegram", "Telegram: @sanspie"), Enabled: true},
|
||||
}})
|
||||
continue
|
||||
}
|
||||
items = append(items, platform.MenuItem{ID: act(func() { reg.Run(id) }), Label: d.Title(), Shortcut: reg.Shortcut(id), Enabled: reg.Enabled(id), Checked: d.Checked != nil && d.Checked()})
|
||||
}
|
||||
if len(items) == 0 {
|
||||
|
||||
@@ -80,7 +80,7 @@ func (a *App) panelCatalog() []workspace.Desc {
|
||||
{Panel: a.diffPanel, Near: workspace.Scene, Order: 216, Icon: "panel.diff"},
|
||||
}
|
||||
if a.exportP != nil {
|
||||
descs = append(descs, workspace.Desc{Panel: a.exportP, Near: workspace.Console, Order: 254, Icon: "file.package"})
|
||||
descs = append(descs, workspace.Desc{Panel: a.exportP, Near: workspace.Console, Order: 254, Icon: "file.export"})
|
||||
}
|
||||
if a.agentP != nil {
|
||||
descs = append(descs, workspace.Desc{Panel: a.agentP, Near: workspace.Inspector, Order: 205, Icon: agentpanel.Icon})
|
||||
|
||||
@@ -31,6 +31,9 @@ const (
|
||||
CommandInstallGo = "help.install_go"
|
||||
recentCommands = 5
|
||||
issuesURL = "https://git.akarpov.ru/sanspie/aego/issues/new"
|
||||
repositoryURL = "https://git.akarpov.ru/sanspie/aego"
|
||||
developerURL = "https://about.akarpov.ru"
|
||||
telegramURL = "https://t.me/sanspie"
|
||||
goDownloadURL = "https://go.dev/dl/"
|
||||
tabRecent = "recent"
|
||||
tabNew = "new"
|
||||
@@ -225,6 +228,12 @@ func (a *App) bugURL() string {
|
||||
return issuesURL + "?body=" + url.QueryEscape(body)
|
||||
}
|
||||
|
||||
func (a *App) openSupportURL(target string) {
|
||||
if err := sysopen.Open(target); err != nil {
|
||||
a.ctx.Notify(context.LevelError, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) runTests() error {
|
||||
a.testsRunning = true
|
||||
a.ctx.Notify(context.LevelInfo, i18n.Note("editor.shell.running_script_tests", "Running script tests…"))
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"aego/editor/core/context"
|
||||
"aego/editor/core/play"
|
||||
"aego/editor/core/workspace"
|
||||
agentpanel "aego/editor/panels/agent"
|
||||
animpanel "aego/editor/panels/animation"
|
||||
"aego/editor/panels/animator"
|
||||
edocs "aego/editor/panels/docs"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"aego/engine/i18n"
|
||||
"aego/platform/input"
|
||||
"aego/ui/core"
|
||||
"aego/ui/dock"
|
||||
"aego/ui/widgets"
|
||||
)
|
||||
|
||||
@@ -39,7 +41,7 @@ func BuiltinWorkspaces() []workspace.Preset {
|
||||
left, browser, center, bottom, right := workspace.SlotLeft, workspace.SlotBrowser, workspace.SlotCenter, workspace.SlotBottom, workspace.SlotRight
|
||||
// These are initial/reset layouts, not migrations. Preserve user tab and splitter state.
|
||||
return []workspace.Preset{
|
||||
{ID: workspace.Design, Title: i18n.Tr("editor.shell.design", "Design"), Order: 0, Explicit: true, Slots: map[string]workspace.Slot{workspace.Hierarchy: left, workspace.Assets: browser, workspace.Scene: center, workspace.Game: center, workspace.Inspector: right, workspace.Problems: bottom, workspace.Console: bottom}, Front: []string{workspace.Scene, workspace.Problems}},
|
||||
{ID: workspace.Design, Title: i18n.Tr("editor.shell.design", "Design"), Order: 0, Explicit: true, Slots: map[string]workspace.Slot{workspace.Hierarchy: left, workspace.Assets: browser, workspace.Scene: center, workspace.Game: center, workspace.Inspector: right, agentpanel.ID: right, workspace.Problems: bottom, workspace.Console: bottom}, Splits: []workspace.PanelSplit{{Panel: agentpanel.ID, Anchor: workspace.Inspector, Side: dock.SideBottom, Ratio: 0.55}}, Front: []string{workspace.Scene, workspace.Problems}},
|
||||
{ID: workspace.Programming, Title: i18n.Tr("editor.shell.programming", "Programming"), Order: 10, Rev: 1, Explicit: true, Slots: map[string]workspace.Slot{workspace.Assets: left, edocs.ID: center, workspace.Game: center, workspace.Problems: bottom, workspace.Console: bottom, scripteditor.FindID: bottom, "agent": right, watch.ID: right}, Front: []string{edocs.ID, workspace.Problems, "agent"}},
|
||||
{ID: workspace.Art, Title: i18n.Tr("editor.shell.art", "Art"), Order: 20, Explicit: true, Slots: map[string]workspace.Slot{workspace.Hierarchy: left, workspace.Assets: browser, workspace.Scene: center, workspace.Game: center, workspace.Inspector: right, animpanel.ID: bottom, animator.ID: bottom}, Front: []string{workspace.Scene, animpanel.ID}},
|
||||
{ID: workspace.Audio, Title: i18n.Tr("editor.shell.audio", "Audio"), Order: 30, Explicit: true, Slots: map[string]workspace.Slot{workspace.Assets: left, workspace.Game: center, workspace.Scene: center, workspace.Inspector: right, workspace.Mixer: bottom, workspace.Console: bottom}, Front: []string{workspace.Game, workspace.Mixer}},
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package shell
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"aego/core/mathx"
|
||||
"aego/editor/core/services"
|
||||
"aego/editor/core/workspace"
|
||||
agentpanel "aego/editor/panels/agent"
|
||||
"aego/ui/dock"
|
||||
)
|
||||
|
||||
type defaultLayoutPanel string
|
||||
|
||||
func (p defaultLayoutPanel) ID() string { return string(p) }
|
||||
func (p defaultLayoutPanel) Title() string { return string(p) }
|
||||
func (p defaultLayoutPanel) Draw(*services.Frame, mathx.Rect) {}
|
||||
|
||||
func TestDesignDefaultAgentAndSavedLayouts(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
makeWorkspace := func(withAgent bool) *workspace.Workspace {
|
||||
w := workspace.New(root, nil)
|
||||
ids := []string{workspace.Hierarchy, workspace.Assets, workspace.Scene, workspace.Game, workspace.Inspector, workspace.Console, workspace.Problems}
|
||||
if withAgent {
|
||||
ids = append(ids, agentpanel.ID)
|
||||
}
|
||||
for i, id := range ids {
|
||||
if err := w.Register(workspace.Desc{Panel: defaultLayoutPanel(id), Order: i}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for _, p := range BuiltinWorkspaces() {
|
||||
if err := w.RegisterPreset(p); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
w.LoadLayout()
|
||||
return w
|
||||
}
|
||||
w := makeWorkspace(true)
|
||||
a, ok := w.Tree.Node(w.Tree.FindPanel(agentpanel.ID))
|
||||
if !ok {
|
||||
t.Fatal("Agent is not open in Design")
|
||||
}
|
||||
i, ok := w.Tree.Node(w.Tree.FindPanel(workspace.Inspector))
|
||||
if !ok || a.Parent != i.Parent {
|
||||
t.Fatal("Agent and Inspector are not in the same split")
|
||||
}
|
||||
split, ok := w.Tree.Node(a.Parent)
|
||||
if !ok || split.Kind != dock.KindSplit || split.Axis != dock.AxisY || split.First != i.ID || split.Second != a.ID || split.Ratio != 0.55 {
|
||||
t.Fatalf("Agent must be below Inspector: %+v", split)
|
||||
}
|
||||
if active, _ := a.ActivePanel(); active != agentpanel.ID {
|
||||
t.Fatalf("Agent is not the visible tab: %q", active)
|
||||
}
|
||||
w.Tree.Undock(agentpanel.ID)
|
||||
if err := w.SaveLayout(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if makeWorkspace(true).Tree.Has(agentpanel.ID) {
|
||||
t.Fatal("the new default replaced a saved user layout")
|
||||
}
|
||||
// No optional Agent panel must still produce a valid default when reset.
|
||||
without := makeWorkspace(false)
|
||||
without.ResetLayout()
|
||||
if !without.Tree.Has(workspace.Inspector) || without.Tree.Has(agentpanel.ID) {
|
||||
t.Fatal("missing optional Agent broke the default layout")
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -10,7 +10,7 @@ import (
|
||||
"aego/platform"
|
||||
)
|
||||
|
||||
var Version = "0.12.0"
|
||||
var Version = "0.12.1"
|
||||
|
||||
type Config struct {
|
||||
Title string
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -53,12 +53,12 @@ title: Editing curves
|
||||
|
||||
The Inspector draws the curve inside the field; a click opens the curve editor.
|
||||
|
||||
- **Preset** switches between presets and **Keys**. Choosing Keys on a preset turns it into editable keys of the same shape.
|
||||
- **Preset** switches between presets and **Keys**. Choosing Keys on a preset approximates its shape with editable keys. You can also drag the preset curve directly: moving it converts the preset to keys and moves the selected point.
|
||||
- **Double-click** the graph to add a key on the curve, **right-click** a key to remove it, drag a key or a slope handle to change it.
|
||||
- Hold **Ctrl** (Cmd on macOS) to snap to a 1/20 grid of the view, hold **Shift** while dragging a slope handle to move one side only.
|
||||
- **Time**, **Value**, **Segment** and **Tangent** edit the selected key; dragging a handle makes its tangent Free.
|
||||
|
||||
Every drag is one undo step. Prefab instances show an overridden curve like any other field.
|
||||
Every drag is one undo step. Undoing a preset drag restores the original preset; Redo restores the edited keys. Prefab instances show an overridden curve like any other field.
|
||||
|
||||
---
|
||||
id: curves/scripts
|
||||
|
||||
@@ -44,12 +44,12 @@ title: Редактирование кривых
|
||||
|
||||
«Инспектор» рисует кривую внутри поля; щелчок открывает редактор кривых.
|
||||
|
||||
- **Пресет** переключает между пресетами и **Ключами**. Если выбрать «Ключи» для пресета, он превращается в редактируемые ключи той же формы.
|
||||
- **Пресет** переключает между пресетами и **Ключами**. Выбор «Ключей» приближает форму пресета редактируемыми ключами. Можно также потянуть сам график пресета: при движении он превращается в ключи, а выбранная точка перемещается.
|
||||
- **Дважды щёлкните** по графику, чтобы добавить ключ на кривой, **щёлкните правой кнопкой** по ключу, чтобы удалить его, перетащите ключ или ручку наклона, чтобы изменить их.
|
||||
- Удерживайте **Ctrl** (Cmd на macOS), чтобы привязываться к сетке в 1/20 вида; удерживайте **Shift**, перетаскивая ручку наклона, чтобы двигать только одну сторону.
|
||||
- **Время**, **Значение**, **Сегмент** и **Касательная** меняют выбранный ключ; перетаскивание ручки делает его касательную свободной.
|
||||
|
||||
Каждое перетаскивание — один шаг отмены. Экземпляры префабов показывают переопределённую кривую, как любое другое поле.
|
||||
Каждое перетаскивание — один шаг отмены. Отмена перетаскивания пресета возвращает исходный пресет, а повтор восстанавливает изменённые ключи. Экземпляры префабов показывают переопределённую кривую, как любое другое поле.
|
||||
|
||||
---
|
||||
id: curves/scripts
|
||||
|
||||
+12
-12
@@ -9,7 +9,7 @@ order: 1
|
||||
|
||||
The editor is where you build scenes, write scripts and run the game. This page tours its parts: workspaces, the theme, editing many nodes, Play and the tools that watch a running game.
|
||||
|
||||

|
||||

|
||||
|
||||
## Workspaces and layout
|
||||
|
||||
@@ -19,13 +19,14 @@ A workspace is a named layout: which panels are docked where and whether the too
|
||||
|
||||
| Workspace | For | Default panels |
|
||||
|---|---|---|
|
||||
| Design | building scenes | Scene and Game in the center, Hierarchy and Assets on the left, Inspector on the right |
|
||||
| Design | building scenes | Scene and Game in the center, Hierarchy and Assets on the left, Inspector and Agent below it on the right |
|
||||
| Programming | writing scripts | Scripts open in the center beside Docs and Game. Problems, Console and Find are below. Agent and Watch are on the right. Play controls are visible by default |
|
||||
| Art | sprites and levels | Scene and Game in the center, Inspector on the right, Animation and Animator below |
|
||||
| Audio | sound work | Game and Scene in the center, Assets on the left, Inspector on the right, Mixer and Console below |
|
||||
|
||||
- Switch with the selector at the right of the toolbar, or with `Ctrl+1` … `Ctrl+4` from any panel, the code editor included.
|
||||
- Rearrange panels freely. Every workspace remembers its own layout. Documents, the selection and Play are shared between workspaces.
|
||||
- Existing saved layouts are left as they are when defaults change. Reset the built-in Design layout if you want its current default panel arrangement.
|
||||
- **Window → Save as Workspace…** keeps the current layout under a new name. You can rename and delete your own workspaces. The built-in ones can only be reset, with **Window → Reset Layout**.
|
||||
- Modules can add workspaces of their own.
|
||||
|
||||
@@ -259,22 +260,22 @@ style_allow: [agent]
|
||||
---
|
||||
# The Agent panel
|
||||
|
||||
The Agent tab, next to the Inspector, talks to Claude Code about the open project. It answers questions, reads the scenes, the scripts and the documentation, and proposes changes that you apply.
|
||||
The Agent panel talks to Claude Code or Codex about the open project. In a new or reset Design workspace it sits below the Inspector. It answers questions, reads the scenes, scripts and documentation, and proposes changes that you apply.
|
||||
|
||||
The model uses the same tools as `aego` and [`aego mcp`](ref:agents), on the editor's open documents. It sees unsaved edits, and what it changes in an open scene or script becomes an Undo step there instead of a write to disk.
|
||||
|
||||
## Setting up
|
||||
|
||||
1. Install Claude Code, the `claude` command.
|
||||
2. Sign in once by running `claude` in a terminal.
|
||||
1. Choose Claude Code or Codex in the provider row.
|
||||
2. Install the selected CLI and sign in once in a terminal. See [Working with AI agents](ref:agents) for the commands and provider settings.
|
||||
|
||||
The panel looks for `claude` on your PATH. When it is not there, the panel says so and takes the path of the program; **Check Again** looks once more. Claude Code only gets the aego tools: no shell and no file access of its own.
|
||||
The panel looks for the selected CLI on your PATH. If it is missing, enter its executable path and use **Check Again**. Claude Code only gets the aego tools: no shell and no file access of its own.
|
||||
|
||||
## Asking
|
||||
|
||||
Type in the field at the bottom and press `Enter`. While the agent answers, **Send** turns into **Stop**, which ends the answer and rejects whatever still waits. Under the field the panel shows what the last answer cost and how long it took.
|
||||
Type in the field at the bottom and press `Enter`. While the agent answers, **Queue** adds another message to send after the current turn; **Stop** ends that answer and pauses queued messages. Under the field the panel shows what the last answer cost and how long it took when available.
|
||||
|
||||
- Type `@` to pick a node of the open scene or an asset. It goes in as a chip; the agent gets the node's id and scene, or the asset's path.
|
||||
- Type `@` in the message to open the picker, search nodes of the open scene or assets, then choose a result or press `Enter`. Its label is inserted at the cursor and a chip appears above the field; the agent gets the node's ID and scene, or the asset's path. `Esc` closes the picker.
|
||||
- Right-click a node in the Hierarchy and pick **Ask Agent…**, or a problem in Problems and pick **Ask Agent to Fix**. The text waits in the field until you send it.
|
||||
- With an empty transcript, the panel offers three examples; a click sends one.
|
||||
|
||||
@@ -310,9 +311,9 @@ The eye button in the header, **Window → Follow the Agent** or `Ctrl+Alt+F` tu
|
||||
- A script step opens the script at its line. The lines a waiting card would change show their new text as ghost lines; once applied, they stay marked in the gutter for a while.
|
||||
- A run or a check brings Problems or the Console forward, with the new entries lit.
|
||||
|
||||
The agent's avatar marks where it works: on the node in the Hierarchy, over it in the Scene View, on the component in the Inspector and on the lines in the code editor. The row under the header says where it is; a click goes there.
|
||||
The agent's avatar marks where it works: on the node in the Hierarchy, over it in the Scene View, on the component in the Inspector and on the lines in the code editor. Hover over the Follow button to see its status and current location. Choose **Show agent location** in the session menu to go there.
|
||||
|
||||
Any click, key or scroll of yours outside the Agent panel pauses following, so the editor never moves under your hands. **Resume Following** goes on at once; after 30 seconds without input it goes on by itself.
|
||||
Any click, key or scroll of yours outside the Agent panel pauses following, so the editor never moves under your hands. Click the Follow button while paused to resume at once; after 30 seconds without input it resumes by itself.
|
||||
|
||||
When you edit a field the agent is about to change, the Inspector says so, and your change is made again after the agent's, as its own Undo step.
|
||||
|
||||
@@ -331,7 +332,7 @@ Each applied change is one Undo step of the scene you edit, named `Agent: …`.
|
||||
|
||||
## Sessions
|
||||
|
||||
The header shows the session by its first message, and what the agent does: idle, thinking, working or waiting for you. The menu next to **Auto-apply** has **New Session**, **History…**, **Plan Only** and **Stop**.
|
||||
The header identifies the session by its first message and shows active states such as thinking, working or waiting for you; hover over the session for its status. Hover over an icon to identify its action, including **Auto-apply** and Follow. The session menu has **New Session**, **History…**, **Plan Only** and **Stop**.
|
||||
|
||||
- **Plan Only** keeps the agent from changing anything: it describes the changes and their order, and you turn the plan into changes when you turn it off.
|
||||
- Each conversation is written to `.aego/user/agent/<date-time>.jsonl`, one line per message, tool call, result and card state. **History…**, or a click on the session name, opens an earlier session read-only. A new message always goes to the current session.
|
||||
@@ -347,4 +348,3 @@ The gauge in the header shows how much of the model's context window the session
|
||||
- **Send with every message** adds the open scene and the selection to each message, so the agent knows what you look at.
|
||||
- **Read limits** keep large reads small: a scene tree up to a number of nodes and levels, with the rest counted, a documentation search up to a number of results, and a script longer than the limit read in parts. They are kept in `prefs.aetext`.
|
||||
- **New Session with Summary** asks the agent for a summary of the session, starts a new one and sends the summary, with the names of the nodes the session changed, with your next message.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ title: Редактор
|
||||
|
||||
В редакторе вы собираете сцены, пишете скрипты и запускаете игру. Эта страница проводит по его частям: раскладки, тема, правка многих узлов сразу, игра и инструменты, которые следят за запущенной игрой.
|
||||
|
||||

|
||||

|
||||
|
||||
## Раскладки и расположение панелей
|
||||
|
||||
@@ -16,13 +16,14 @@ title: Редактор
|
||||
|
||||
| Раскладка | Для чего | Панели по умолчанию |
|
||||
|---|---|---|
|
||||
| Дизайн | сборка сцен | Сцена и Игра в центре, Иерархия и Ассеты слева, Инспектор справа |
|
||||
| Дизайн | сборка сцен | Сцена и Игра в центре, Иерархия и Ассеты слева, справа Инспектор и под ним Агент |
|
||||
| Программирование | написание скриптов | Скрипты открываются в центре рядом с Документацией и Игрой. Внизу расположены «Проблемы», «Консоль» и «Найти», справа — «Агент» и «Отслеживание». Кнопки управления игрой видны по умолчанию |
|
||||
| Графика | спрайты и уровни | Сцена и Игра в центре, Инспектор справа, Анимация и Аниматор внизу |
|
||||
| Звук | работа со звуком | Игра и Сцена в центре, Ассеты слева, Инспектор справа, Микшер и Консоль внизу |
|
||||
|
||||
- Переключайте раскладки списком справа на панели инструментов или клавишами `Ctrl+1` … `Ctrl+4` из любой панели, в том числе из редактора кода.
|
||||
- Переставляйте панели как угодно. Каждая раскладка помнит своё расположение. Документы, выделение и игра общие для всех раскладок.
|
||||
- Сохранённые раскладки не меняются при обновлении настроек по умолчанию. Сбросьте встроенную раскладку «Дизайн», если хотите получить её текущее расположение панелей.
|
||||
- **Окно → Сохранить как раскладку…** сохраняет текущее расположение под новым именем. Свои раскладки можно переименовывать и удалять. Встроенные можно только сбросить командой **Окно → Сбросить расположение панелей**.
|
||||
- Модули могут добавлять свои раскладки.
|
||||
|
||||
@@ -249,22 +250,22 @@ title: Панель «Агент»
|
||||
---
|
||||
# Панель «Агент»
|
||||
|
||||
Вкладка «Агент» рядом с Инспектором общается с Claude Code об открытом проекте. Он отвечает на вопросы, читает сцены, скрипты и документацию и предлагает изменения, которые применяете вы.
|
||||
Панель «Агент» общается с Claude Code или Codex об открытом проекте. В новой или сброшенной раскладке «Дизайн» она находится под Инспектором. Агент отвечает на вопросы, читает сцены, скрипты и документацию и предлагает изменения, которые применяете вы.
|
||||
|
||||
Модель пользуется теми же инструментами, что `aego` и [`aego mcp`](ref:agents), но над открытыми документами редактора. Она видит несохранённые правки, а то, что она меняет в открытой сцене или скрипте, становится там шагом отмены, а не записью на диск.
|
||||
|
||||
## Настройка
|
||||
|
||||
1. Установите Claude Code — команду `claude`.
|
||||
2. Один раз войдите, запустив `claude` в терминале.
|
||||
1. Выберите Claude Code или Codex в строке провайдера.
|
||||
2. Установите выбранный CLI и один раз войдите через терминал. Команды и настройки провайдеров описаны в разделе [Работа с ИИ-агентами](ref:agents).
|
||||
|
||||
Панель ищет `claude` в PATH. Если его там нет, панель сообщает об этом и принимает путь к программе; **Проверить снова** ищет ещё раз. Claude Code получает только инструменты aego: ни оболочки, ни собственного доступа к файлам.
|
||||
Панель ищет выбранный CLI в PATH. Если его там нет, укажите путь к программе и нажмите **Проверить снова**. Claude Code получает только инструменты aego: ни оболочки, ни собственного доступа к файлам.
|
||||
|
||||
## Вопросы агенту
|
||||
|
||||
Пишите в поле внизу и нажимайте `Enter`. Пока агент отвечает, **Отправить** превращается в **Остановить**: ответ завершается, а всё, что ещё ждёт, отклоняется. Под полем панель показывает, сколько стоил последний ответ и сколько он занял.
|
||||
Пишите в поле внизу и нажимайте `Enter`. Пока агент отвечает, кнопка очереди добавляет следующее сообщение после текущего ответа; **Остановить** завершает ответ и ставит очередь на паузу. Под полем, если сведения доступны, показаны стоимость и длительность последнего ответа.
|
||||
|
||||
- Наберите `@`, чтобы выбрать узел открытой сцены или ассет. Он вставляется чипом; агент получает id узла и его сцену или путь к ассету.
|
||||
- Наберите `@` в сообщении, найдите узел открытой сцены или ассет и выберите результат либо нажмите `Enter`. Название вставляется в позицию курсора, а над полем появляется чип; агент получает ID узла и его сцену или путь к ассету. `Esc` закрывает список.
|
||||
- Щёлкните узел в Иерархии правой кнопкой и выберите **Спросить агента…** или проблему в «Проблемах» и выберите **Попросить агента исправить**. Текст ждёт в поле, пока вы его не отправите.
|
||||
- Пока лента пуста, панель предлагает три примера; щелчок отправляет пример.
|
||||
|
||||
@@ -300,9 +301,9 @@ title: Панель «Агент»
|
||||
- Шаг по скрипту открывает скрипт на нужной строке. Строки, которые изменит ждущая карточка, показывают новый текст призрачными строками; после применения они ещё какое-то время отмечены в гаттере.
|
||||
- Запуск или проверка выводят вперёд «Проблемы» или «Консоль» с подсвеченными новыми записями.
|
||||
|
||||
Аватар агента отмечает, где он работает: на узле в Иерархии, над ним в окне сцены, на компоненте в Инспекторе и на строках в редакторе кода. Строка под шапкой говорит, где он сейчас; щелчок ведёт туда.
|
||||
Аватар агента отмечает, где он работает: на узле в Иерархии, над ним в окне сцены, на компоненте в Инспекторе и на строках в редакторе кода. Наведите на кнопку слежки, чтобы увидеть её состояние и текущее место работы. Команда **Показать место работы агента** в меню сеанса ведёт туда.
|
||||
|
||||
Любой ваш щелчок, клавиша или прокрутка вне панели «Агент» ставят слежку на паузу, чтобы редактор не двигался у вас под руками. **Продолжить слежку** включает её сразу, а через 30 секунд без ввода она продолжается сама.
|
||||
Любой ваш щелчок, клавиша или прокрутка вне панели «Агент» ставят слежку на паузу, чтобы редактор не двигался у вас под руками. Нажмите кнопку слежки во время паузы, чтобы продолжить сразу; через 30 секунд без ввода она продолжается сама.
|
||||
|
||||
Когда вы правите поле, которое агент вот-вот изменит, Инспектор говорит об этом, и ваша правка повторяется после правки агента отдельным шагом отмены.
|
||||
|
||||
@@ -321,7 +322,7 @@ title: Панель «Агент»
|
||||
|
||||
## Сеансы
|
||||
|
||||
Шапка показывает сеанс по его первому сообщению и то, чем занят агент: свободен, думает, работает или ждёт вас. В меню рядом с **Автоприменением** есть **Новый сеанс**, **История…**, **Только план** и **Остановить**.
|
||||
Шапка называет сеанс по первому сообщению и показывает активные состояния: агент думает, работает или ждёт вас. Наведите на название сеанса, чтобы увидеть его состояние, а на значок — чтобы узнать его действие, например **Автоприменение** или слежка. В меню сеанса есть **Новый сеанс**, **История…**, **Только план** и **Остановить**.
|
||||
|
||||
- **Только план** не даёт агенту ничего менять: он описывает изменения и их порядок, а превратить план в изменения можно, выключив режим.
|
||||
- Каждый разговор пишется в `.aego/user/agent/<date-time>.jsonl`, по строке на сообщение, вызов инструмента, результат и состояние карточки. **История…** или щелчок по имени сеанса открывает прошлый сеанс только для чтения. Новое сообщение всегда идёт в текущий сеанс.
|
||||
@@ -337,4 +338,3 @@ title: Панель «Агент»
|
||||
- **Отправлять с каждым сообщением** добавляет к каждому сообщению открытую сцену и выделение, чтобы агент знал, на что вы смотрите.
|
||||
- **Лимиты чтения** не дают большим чтениям раздувать контекст: дерево сцены — до заданного числа узлов и уровней, остальное посчитано; поиск по документации — до заданного числа результатов; скрипт длиннее лимита читается по частям. Лимиты хранятся в `prefs.aetext`.
|
||||
- **Новый сеанс со сводкой** просит агента написать сводку сеанса, начинает новый и отправляет сводку вместе с именами изменённых в сеансе узлов с вашим следующим сообщением.
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user