Compare commits
34
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f3b06af54 | ||
|
|
788dae1bf7 | ||
|
|
3812145c74 | ||
|
|
bfc69d5598 | ||
|
|
a946785c7b | ||
|
|
2104d1d54c | ||
|
|
6c1d19704e | ||
|
|
579751f54a | ||
|
|
87e3181a2a | ||
|
|
199d39b347 | ||
|
|
43cbb6bb7f | ||
|
|
8cfb81d071 | ||
|
|
2687450c2d | ||
|
|
796fe5b52f | ||
|
|
4924bb12e8 | ||
|
|
54c1af7edf | ||
|
|
113892ab67 | ||
|
|
5e55c2dbb9 | ||
|
|
c64c199e70 | ||
|
|
aa1da698f4 | ||
|
|
25a731f6c4 | ||
|
|
5a2745987f | ||
|
|
8a364bee5e | ||
|
|
3d2cf44608 | ||
|
|
eb92b49531 | ||
|
|
09d10625c3 | ||
|
|
05aa019738 | ||
|
|
d965aeb886 | ||
|
|
32f65c3ddc | ||
|
|
8fd0483b42 | ||
|
|
029f6d7cb4 | ||
|
|
07a6fa1e55 | ||
|
|
6b52dd790b | ||
|
|
3c015e9266 |
@@ -1,5 +0,0 @@
|
||||
# Golden files and project sources are byte-stable across platforms.
|
||||
* text=auto eol=lf
|
||||
|
||||
# Preserve the upstream font license exactly as committed (it uses CRLF).
|
||||
tools/gendocs/site/static/fonts/IBM-Plex-OFL.txt -text
|
||||
+1
-39
@@ -1,41 +1,3 @@
|
||||
# CI workflows
|
||||
|
||||
Gitea is the source of truth. The Linux workflow runs there; optional Gitea
|
||||
platform runners remain controlled by `PLATFORM_RUNNERS`.
|
||||
|
||||
`github-native.yml` is for the GitHub mirror. It builds and tests on native
|
||||
Windows, macOS (arm64 and amd64), and Linux runners, and uploads unsigned CI
|
||||
artifacts and diagnostic logs. The Windows installer smoke is restricted to an
|
||||
ephemeral GitHub-hosted runner. macOS bundles are ad-hoc signed, not notarized.
|
||||
These jobs are not evidence of platform support until they actually pass on
|
||||
their respective runners.
|
||||
|
||||
On Gitea, the GitHub-only matrix routes to the Linux runner before being
|
||||
skipped. This prevents Windows and macOS entries from waiting indefinitely for
|
||||
runners that Gitea does not have. On GitHub, the actual native labels are used.
|
||||
macOS and Windows build the pinned GLSL reference validator from verified
|
||||
source, rather than depending on a changing package-manager version.
|
||||
|
||||
To show native GitHub results on the Gitea commit, create the GitHub Actions
|
||||
secret `GITEA_STATUS_TOKEN` containing a Gitea API token with repository write
|
||||
permission. Prefer a non-admin bot that has access only to `sanspie/aego`:
|
||||
Gitea's repository-write token is broader than commit statuses. The reporter
|
||||
publishes pending and final statuses with links to the real GitHub jobs only
|
||||
for trusted `master` pushes or manual runs, never for pull requests. If the
|
||||
secret is missing, CI still runs but no Gitea statuses are published.
|
||||
|
||||
Keep GitHub Actions disabled through the first mirror sync: historical tags
|
||||
can contain older workflows. Before enabling Actions, verify mirrored refs and
|
||||
restrict the allowed workflows so historical release jobs cannot run. Use a
|
||||
separate repository-scoped credential for the mirror; do not put it in a URL.
|
||||
Gitea's build-and-publish job remains paused. `github-release-mirror.yml`
|
||||
copies already-published Gitea releases and assets to GitHub after the current
|
||||
master passes Gitea Linux CI and all four real GitHub native jobs. It also
|
||||
checks periodically and can be dispatched manually from master. Versioned
|
||||
stable releases are published normally, with the highest stable version marked
|
||||
Latest; nightly, beta versions, and the stable-channel pointer are prereleases.
|
||||
The mirror verifies matching git refs and asset sizes/checksums, stages new
|
||||
releases as drafts, and never moves tags or replaces existing assets. If a
|
||||
mutable nightly or another mirrored snapshot changes, the mirror stops rather
|
||||
than overwriting it; a separate reviewed update is needed in that case. Signed
|
||||
update manifests are copied unchanged and retain their original download URLs.
|
||||
The workflows in this directory automate checks, builds, tests, and release publishing for Aego. Optional platform-runner jobs cover Windows and macOS alongside the main Linux checks.
|
||||
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
# Native runs on Windows and macOS machines; the Windows build itself is cross-compiled on Linux in ci.yml (D301).
|
||||
smoke:
|
||||
name: ${{ matrix.goos }} ${{ matrix.goarch }} smoke
|
||||
if: github.server_url != 'https://github.com' && vars.PLATFORM_RUNNERS == 'true'
|
||||
if: vars.PLATFORM_RUNNERS == 'true'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
|
||||
@@ -20,7 +20,6 @@ env:
|
||||
|
||||
jobs:
|
||||
checks:
|
||||
if: github.server_url != 'https://github.com'
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -45,7 +44,6 @@ jobs:
|
||||
run: make nsis-check
|
||||
|
||||
build:
|
||||
if: github.server_url != 'https://github.com'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -123,9 +121,7 @@ jobs:
|
||||
|
||||
release:
|
||||
needs: [checks, build]
|
||||
# Pause all automatic releases until the reviewed nightly/stable pipeline
|
||||
# enforces native gates and preserves existing tags and release assets.
|
||||
if: ${{ false }}
|
||||
if: github.event_name == 'push'
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,37 +0,0 @@
|
||||
name: github-release-mirror
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [github-native]
|
||||
types: [completed]
|
||||
branches: [master]
|
||||
schedule:
|
||||
- cron: '17 */6 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: github-release-mirror
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
mirror:
|
||||
if: github.server_url == 'https://github.com' && github.repository == 'Alexander-D-Karpov/aego' && github.ref == 'refs/heads/master'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: write
|
||||
actions: read
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
||||
with:
|
||||
ref: refs/heads/master
|
||||
persist-credentials: false
|
||||
- name: Test release mirror
|
||||
run: python3 -m unittest discover -s tools/ci -p mirror_releases_test.py -v
|
||||
- name: Mirror published Gitea releases
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: python3 tools/ci/mirror_releases.py --apply
|
||||
@@ -47,6 +47,7 @@ MODULE_BENCH_COMPARE := ./tests/integration:RewindScrub
|
||||
BENCH_PHYSICS_PKGS :=
|
||||
.DEFAULT_GOAL := bootstrap
|
||||
-include modules/*/module.mk
|
||||
include tools/artstudio/release/release.mk
|
||||
|
||||
.PHONY: materials materials-check dmg nsis-check test-update clean-root appimage bootstrap build test test-nonative test-editor test-runtime test-gl vet lint depcheck smoke run genfont genicons icons icons-doc res genpreviews golden golden-update site-headless bench bench-baseline bench-scripts bench-build bench-compare bench-profile bench-scripts-run bench-allocs playground playground-size site site-check cli example example-platformer check-all ci clean fuzz runtime export-smoke editor-app shaders test-wasm web-smoke web-smoke-update web-bench web-bench-baseline try-size shots shots-check affected quick check-camera check-audio check-gameplay check-mutate check-editor check-web check-docs i18n-check pkgsize
|
||||
|
||||
@@ -102,7 +103,8 @@ test-nonative:
|
||||
# Tests needing a real GL context (offscreen SDL + llvmpipe).
|
||||
test-gl: bootstrap
|
||||
$(GL_ENV) go test -tags aego_gltest ./tests/gl/
|
||||
$(GL_ENV) go test -tags aego_gltest ./app/edit -count=1 -run '^Test((Material|Lighting|ImageAsset)AuthoringWorkflowNative|PlatformerVignetteGameView|PostFXEditPreviewWithoutPlay|CameraInspectorShowsRenderedPostStatus|PostFXAndParticlePresetThumbnails|ColdPostMaterialPreviewAndThumbnail|DiagnosticComposerConsoleProfilerNative)$$'
|
||||
$(GL_ENV) go test -tags aego_gltest ./app/edit -count=1 -run '^Test((Material|Lighting|ImageAsset|ArtScene)AuthoringWorkflowNative|ArtWorkingAlphaShadowNative|PlatformerVignetteGameView|PostFXEditPreviewWithoutPlay|CameraInspectorShowsRenderedPostStatus|PostFXAndParticlePresetThumbnails|ColdPostMaterialPreviewAndThumbnail|DiagnosticComposerConsoleProfilerNative)$$'
|
||||
$(GL_ENV) go test -tags aego_editor,aego_gltest -count=1 -run '^(Test(Mesh2D|SpriteRenderer)TryNativeVisibleOrderAndReset|TestComponentContractProbeRunsNativeAndRejectsUnrunWeb|TestBrowserProcessDeadlineFailsClosed)$$' ./app/docsview
|
||||
$(GL_ENV) SDL_AUDIO_DRIVER=dummy AEGO_PLATFORMER_SHOTS=$(CURDIR)/.cache/stage12/lighting-route AEGO_PARTICLE_SHOTS=$(CURDIR)/.cache/stage12/particle-route AEGO_POSTFX_SHOTS=$(CURDIR)/.cache/stage12/postfx go test -tags aego_gltest -count=1 -run GL\|Screenshots ./tests/integration/
|
||||
|
||||
vet: bootstrap
|
||||
@@ -383,6 +385,7 @@ web-visibility-lifecycle-test:
|
||||
node --test tools/websmoke/visibility-browser.test.mjs
|
||||
|
||||
ci: web-warning-policy-test web-bench-baseline-test web-audio-policy-test web-visibility-lifecycle-test shader-validator materials-check build vet lint depcheck i18n-check test test-nonative test-editor test-runtime test-wasm runtime smoke fuzz golden shots-check test-gl cli playground web-smoke site-check example example-platformer check-all export-smoke test-update $(MODULE_CI) bench-allocs
|
||||
ci: studio-web-test
|
||||
@sh tools/ci-clean-root.sh
|
||||
|
||||
# test-update builds two versions of the CLI and updates one to the other through a release server on loopback (D362).
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"aego/art"
|
||||
)
|
||||
|
||||
// ArtHost is supplied only by the running editor. Offline commands must not
|
||||
// reopen files and silently replace the user's dirty graphical documents.
|
||||
// Like Host.ApplyScene, these calls run on the editor mutation thread.
|
||||
type ArtHost interface {
|
||||
ReadArt(file, section string, offset, limit int) (*Output, error)
|
||||
CaptureArt(file string, frame art.ID, channel art.Channel) (*Output, error)
|
||||
PrepareArt(file string, revision uint64, label string, ops []art.Operation) (*Output, error)
|
||||
}
|
||||
|
||||
// ArtProposal is an opaque, prepared transaction carried by the existing agent
|
||||
// proposal queue. Commit is invoked only after its normal approval decision.
|
||||
// Keeping the prepared transaction also preserves IDs returned by its preview.
|
||||
type ArtProposal interface {
|
||||
Commit() (*Output, error)
|
||||
RetainedBytes() int64
|
||||
}
|
||||
|
||||
func artHost(s *Session) (ArtHost, error) {
|
||||
if s != nil {
|
||||
if h, ok := s.host.(ArtHost); ok {
|
||||
return h, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("art: connect to the running Aego editor; offline writes to graphical documents are disabled")
|
||||
}
|
||||
func artCommands() []*Command {
|
||||
fileArg := Arg{Name: "file", Type: "string", Doc: "Project-relative .aeart source, including unsaved editor content", Required: true, Pos: true}
|
||||
return []*Command{
|
||||
{Name: "art.read", Doc: "Read the current art document, not its last saved file. Lists are paginated; revision is a decimal string for art.apply.", Args: []Arg{
|
||||
fileArg, {Name: "section", Type: "string", Doc: "summary, layers, frames, cels, palette, tags or slices"},
|
||||
{Name: "offset", Type: "integer", Doc: "First item, default 0"}, {Name: "limit", Type: "integer", Doc: "1..1024 items; default 256"},
|
||||
}, Run: func(s *Session, a Args) (*Output, error) {
|
||||
h, err := artHost(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
offset, err := a.Int("offset", 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
limit, err := a.Int("limit", 256)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if offset < 0 || limit < 1 || limit > 1024 {
|
||||
return nil, fmt.Errorf("art: invalid page")
|
||||
}
|
||||
return h.ReadArt(a.Str("file"), a.Str("section"), offset, limit)
|
||||
}},
|
||||
{Name: "art.capture", Doc: "Capture unsaved CPU pixels with document/frame/channel identity and revision. MCP returns a PNG image; the CLI writes it only with --out to a new file.", Args: []Arg{
|
||||
fileArg, {Name: "frame", Type: "string", Doc: "Stable frame ID; defaults to current frame"},
|
||||
{Name: "channel", Type: "string", Doc: "albedo, normal, height or emission; defaults to current channel"},
|
||||
}, Run: func(s *Session, a Args) (*Output, error) {
|
||||
h, err := artHost(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return h.CaptureArt(a.Str("file"), art.ID(a.Str("frame")), art.Channel(a.Str("channel")))
|
||||
}},
|
||||
{Name: "art.apply", Doc: "Propose one atomic graphics edit at the revision returned by art.read. Agent panel edits require approval; CLI and external agents follow the editor's confirmation setting. All use the document's common Undo. Name new entities with as and refer to them as $name later.", Write: true, Destroys: true, Args: []Arg{
|
||||
fileArg, {Name: "revision", Type: "string", Doc: "Exact decimal revision returned by art.read", Required: true},
|
||||
{Name: "label", Type: "string", Doc: "Human-readable operation label", Required: true},
|
||||
{Name: "operations", Type: "array", Doc: "1..4096 typed art.Operation objects; no paths or shell commands", Required: true, Items: map[string]any{"type": "object"}},
|
||||
}, Run: func(s *Session, a Args) (*Output, error) {
|
||||
h, err := artHost(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !a.Bool("dry-run") {
|
||||
return nil, fmt.Errorf("art: edits must pass through the editor proposal queue")
|
||||
}
|
||||
revision, err := strconv.ParseUint(a.Str("revision"), 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("art: revision must be an unsigned decimal integer")
|
||||
}
|
||||
label := strings.TrimSpace(a.Str("label"))
|
||||
if label == "" || len(label) > 256 {
|
||||
return nil, fmt.Errorf("art: label must have 1..256 bytes")
|
||||
}
|
||||
ops, err := art.DecodeOperations([]byte(a.Str("operations")))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return h.PrepareArt(a.Str("file"), revision, label, ops)
|
||||
}},
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"aego/project"
|
||||
@@ -14,6 +15,7 @@ type cliFlags struct {
|
||||
json bool
|
||||
quiet bool
|
||||
strict bool
|
||||
artOut string
|
||||
}
|
||||
|
||||
func Usage(w io.Writer) {
|
||||
@@ -101,6 +103,22 @@ func parseArgs(c *Command, toks []string) (Args, cliFlags, error) {
|
||||
i++
|
||||
}
|
||||
continue
|
||||
case "out":
|
||||
if c.Name != "art.capture" {
|
||||
break
|
||||
}
|
||||
if !has {
|
||||
if i+1 >= len(toks) {
|
||||
return nil, f, fmt.Errorf("--out needs a value")
|
||||
}
|
||||
i++
|
||||
val = toks[i]
|
||||
}
|
||||
if val == "" || f.artOut != "" {
|
||||
return nil, f, fmt.Errorf("--out requires one non-empty path")
|
||||
}
|
||||
f.artOut = val
|
||||
continue
|
||||
}
|
||||
x, ok := byName[name]
|
||||
if !ok {
|
||||
@@ -129,6 +147,9 @@ func parseArgs(c *Command, toks []string) (Args, cliFlags, error) {
|
||||
}
|
||||
|
||||
func report(stdout, stderr io.Writer, f cliFlags, out *Output, err error) int {
|
||||
if err == nil && f.artOut != "" {
|
||||
err = writeArtCapture(f.artOut, out)
|
||||
}
|
||||
switch {
|
||||
case f.json:
|
||||
var v any = out
|
||||
@@ -151,3 +172,26 @@ func report(stdout, stderr io.Writer, f cliFlags, out *Output, err error) int {
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// An explicit output path never overwrites a user's existing file.
|
||||
func writeArtCapture(path string, out *Output) error {
|
||||
if out == nil || len(out.Images) != 1 || out.Images[0].Mime != "image/png" {
|
||||
return errors.New("art: editor did not return a PNG capture")
|
||||
}
|
||||
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("art: cannot create capture: %w", err)
|
||||
}
|
||||
n, err := f.Write(out.Images[0].Data)
|
||||
if err == nil && n != len(out.Images[0].Data) {
|
||||
err = io.ErrShortWrite
|
||||
}
|
||||
if closeErr := f.Close(); err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
if err != nil {
|
||||
_ = os.Remove(path)
|
||||
return fmt.Errorf("art: cannot write capture: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ type Output struct {
|
||||
// Images go to the model as image content, such as the screenshots of a run.
|
||||
Images []Image `json:"-"`
|
||||
Drawing *DrawingEdit `json:"-"`
|
||||
Art ArtProposal `json:"-"`
|
||||
}
|
||||
|
||||
// Image is a picture an answer carries.
|
||||
@@ -73,6 +74,7 @@ func catalog() []*Command {
|
||||
out = append(out, materialCommands()...)
|
||||
out = append(out, postEffectCommands()...)
|
||||
out = append(out, textureCommands()...)
|
||||
out = append(out, artCommands()...)
|
||||
out = append(out, schemaCommands()...)
|
||||
out = append(out, projectCommands()...)
|
||||
out = append(out, scriptCommands()...)
|
||||
@@ -82,7 +84,7 @@ func catalog() []*Command {
|
||||
if c.Name == "asset.import-variant" {
|
||||
c.Look = ls["asset.pixels"]
|
||||
}
|
||||
if c.Look.Title == nil && (strings.HasPrefix(c.Name, "postfx.") || strings.HasPrefix(c.Name, "material.") || strings.HasPrefix(c.Name, "shader.") || strings.HasPrefix(c.Name, "texture.")) {
|
||||
if c.Look.Title == nil && (strings.HasPrefix(c.Name, "postfx.") || strings.HasPrefix(c.Name, "material.") || strings.HasPrefix(c.Name, "shader.") || strings.HasPrefix(c.Name, "texture.") || strings.HasPrefix(c.Name, "art.")) {
|
||||
name := strings.ReplaceAll(c.Name, ".", " ")
|
||||
c.Look = Look{Icon: "comp.sprite", Title: func(a Args) string { return name + " " + file(a) }, Summary: changes}
|
||||
}
|
||||
@@ -143,6 +145,9 @@ func (c *Command) Usage() string {
|
||||
if c.Write {
|
||||
parts = append(parts, "[--dry-run]")
|
||||
}
|
||||
if c.Name == "art.capture" {
|
||||
parts = append(parts, "[--out path]")
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"aego/art"
|
||||
"aego/asset"
|
||||
)
|
||||
|
||||
// Source-only scene validation must not treat a partial sprite sheet as a
|
||||
// complete generic material texture when native runtime rejects that sampler.
|
||||
func TestAgentSourceMaterialRejectsPartialArtSheet(t *testing.T) {
|
||||
for _, multi := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("multi=%v", multi), func(t *testing.T) {
|
||||
s := materialSession(t)
|
||||
doc, err := art.New(4, 4, art.RGBA, art.DefaultLimits())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer doc.Close()
|
||||
if multi {
|
||||
edit, err := doc.Begin(doc.Revision(), "Add frame")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := edit.AddFrame(doc.Info().Frames[0].ID, 100); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
change, err := edit.Prepare()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := change.Apply(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
data, err := doc.Encode()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
artID, err := s.create("assets/art.aeart", data)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
content := fmt.Sprintf("material version=1 shader=asset(%q) { params { Noise=asset(%q) } }", asset.BuiltinShaderDissolve.String(), artID.String())
|
||||
materialID, err := s.create("assets/art.aemat", []byte(content))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if info, ok := (sources{s.Pipeline}).TextureMetadata(artID); !ok || info.Region != multi || info.Width != 4 || info.Height != 4 {
|
||||
t.Fatalf("source-only metadata lost first-frame bounds: %+v/%v", info, ok)
|
||||
}
|
||||
if _, ok := (sources{s.Pipeline}).Material(materialID); ok == multi {
|
||||
t.Fatalf("source-only material resolver result=%v for multi=%v", ok, multi)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -470,6 +470,23 @@ func (r sources) Material(id ids.AssetID) (*material.Material, bool) {
|
||||
}
|
||||
e, ok := r.pl.Entry(tid)
|
||||
info, valid := r.pl.TextureSourceInfo(tid, nil)
|
||||
if !valid && strings.EqualFold(path.Ext(e.Source), ".aeart") {
|
||||
// The PNG source inspector cannot decode an art document. Use the
|
||||
// same prepared frame semantics as the runtime instead of silently
|
||||
// accepting its whole animation sheet as a generic sampler.
|
||||
prepared, ready := r.TextureMetadata(tid)
|
||||
if !ok || e.Missing || e.Error != "" || !ready {
|
||||
return material.Tex{}, false
|
||||
}
|
||||
mode := texture.EmissionNone
|
||||
switch prepared.Semantic {
|
||||
case asset.TextureEmissionRGB:
|
||||
mode = texture.EmissionRGB
|
||||
case asset.TextureEmissionMask:
|
||||
mode = texture.EmissionMask
|
||||
}
|
||||
return material.Tex{RegionOnly: prepared.Region, NormalMap: prepared.Semantic == asset.TextureNormal, EmissionMode: mode}, true
|
||||
}
|
||||
mode := texture.EmissionNone
|
||||
switch info.Role {
|
||||
case asset.RoleEmissionRGB:
|
||||
|
||||
@@ -41,7 +41,7 @@ var versions = []string{"2025-06-18", "2025-03-26", "2024-11-05"}
|
||||
|
||||
var skipDirs = []string{".aego", ".git", "exports", "native"}
|
||||
|
||||
const instructions = `Tools for one Aego project. Paths are relative to the project root; a node is a path from the root (Player/Sprite) or #<id> from scene_tree; values use scene syntax: 2.5, [1, 2], #ff8800ff, Kinematic, "text", asset("<id>") or a project path. Read schema_show before setting fields of an unfamiliar component and docs_search / docs_read for how the engine works. Every write tool takes dry-run; scene_apply applies a whole transaction at once. Check results with scene_validate, project_validate and scene_run. A sprite with no texture yet is drawn, not built from colored rectangles: asset_pixels takes a character grid and a palette (frames give an atlas and a flipbook), asset_svg smooth shapes for tiles, UI and icons; both answer with a preview to check. Draw in the style of project.aego's art section (project_get art): its palette, and without a palette argument 0-9 and a-z are its colors.`
|
||||
const instructions = `Tools for one Aego project. Paths are relative to the project root; a node is a path from the root (Player/Sprite) or #<id> from scene_tree; values use scene syntax: 2.5, [1, 2], #ff8800ff, Kinematic, "text", asset("<id>") or a project path. Read schema_show before setting fields of an unfamiliar component and docs_search / docs_read for how the engine works. Every write tool takes dry-run; scene_apply applies a whole transaction at once. For editable .aeart in a running editor, art_read and art_capture inspect the unsaved document; art_apply proposes an undoable batch at the returned revision. Check results with scene_validate, project_validate and scene_run. A sprite with no texture yet is drawn, not built from colored rectangles: asset_pixels takes a character grid and a palette (frames give an atlas and a flipbook), asset_svg smooth shapes for tiles, UI and icons; both answer with a preview to check. Draw in the style of project.aego's art section (project_get art): its palette, and without a palette argument 0-9 and a-z are its colors.`
|
||||
|
||||
type request struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
|
||||
+17
-1
@@ -3,6 +3,7 @@ package agent
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -91,7 +92,7 @@ func proxyRun(addr, token string, c *Command, a Args) (*Output, error) {
|
||||
var rep struct {
|
||||
Result struct {
|
||||
Content []struct {
|
||||
Type, Text string
|
||||
Type, Text, Data, MimeType string
|
||||
} `json:"content"`
|
||||
IsError bool `json:"isError"`
|
||||
} `json:"result"`
|
||||
@@ -122,6 +123,21 @@ func proxyRun(addr, token string, c *Command, a Args) (*Output, error) {
|
||||
return &Output{OK: true, Text: text}, nil
|
||||
}
|
||||
o := out.Output
|
||||
if c.Name == "art.capture" {
|
||||
for _, part := range rep.Result.Content {
|
||||
if part.Type != "image" || part.MimeType != "image/png" {
|
||||
continue
|
||||
}
|
||||
if len(part.Data) > base64.StdEncoding.EncodedLen(16<<20) {
|
||||
return nil, errors.New("art: PNG exceeds agent capture budget")
|
||||
}
|
||||
data, err := base64.StdEncoding.DecodeString(part.Data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("art: invalid PNG response: %w", err)
|
||||
}
|
||||
o.Images = append(o.Images, Image{Mime: part.MimeType, Data: data})
|
||||
}
|
||||
}
|
||||
var s string
|
||||
switch {
|
||||
case c.Name == "scene.run":
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package bootstrap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
@@ -99,8 +98,7 @@ func TestKeyTracksNativeSources(t *testing.T) {
|
||||
|
||||
func TestLocalReplaces(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
absolute := filepath.VolumeName(root) + "/abs/c"
|
||||
writeFile(t, filepath.Join(root, "go.mod"), fmt.Sprintf(`module game
|
||||
writeFile(t, filepath.Join(root, "go.mod"), `module game
|
||||
|
||||
go 1.27
|
||||
|
||||
@@ -109,9 +107,9 @@ replace aego => ../engine // local engine
|
||||
replace (
|
||||
example.com/a v1.2.3 => ./vendor/a
|
||||
example.com/b => example.com/b v1.0.0
|
||||
example.com/c => %s
|
||||
example.com/c => /abs/c
|
||||
)
|
||||
`, absolute))
|
||||
`)
|
||||
got, err := LocalReplaces(root)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -119,7 +117,7 @@ replace (
|
||||
want := map[string]bool{
|
||||
filepath.Clean(filepath.Join(root, "../engine")): true,
|
||||
filepath.Clean(filepath.Join(root, "vendor", "a")): true,
|
||||
filepath.Clean(absolute): true,
|
||||
filepath.Clean("/abs/c"): true,
|
||||
}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("replaces = %v", got)
|
||||
|
||||
@@ -45,6 +45,9 @@ type Config struct {
|
||||
Platform platform.Platform
|
||||
Profile string
|
||||
NoProfile bool
|
||||
// NoExternalImportTools prevents project assets from starting external
|
||||
// converters and validators, including on later reimport.
|
||||
NoExternalImportTools bool
|
||||
// SharedIndex opens the asset index read-only without a warning when another process, the editor, holds it.
|
||||
SharedIndex bool
|
||||
}
|
||||
@@ -134,6 +137,9 @@ func (h *Host) build(cfg Config, mods []module.Module) error {
|
||||
if h.Importers, err = DevImporters(h.Workspace, h.Modules); err != nil {
|
||||
return err
|
||||
}
|
||||
if cfg.NoExternalImportTools {
|
||||
disableExternalImportTools(h.Importers)
|
||||
}
|
||||
if err = h.Types.Schema().Freeze(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+3
-13
@@ -78,21 +78,11 @@ type Bundle struct {
|
||||
func sanitizer() func(string) string {
|
||||
home, _ := os.UserHomeDir()
|
||||
return func(s string) string {
|
||||
return hideHome(s, home)
|
||||
}
|
||||
}
|
||||
|
||||
func hideHome(s, home string) string {
|
||||
if home == "" {
|
||||
if home != "" {
|
||||
s = strings.ReplaceAll(s, home, "~")
|
||||
}
|
||||
return s
|
||||
}
|
||||
// Windows paths can appear with either separator in stack traces and logs.
|
||||
for _, prefix := range [2]string{home, strings.ReplaceAll(home, `\`, "/")} {
|
||||
s = strings.ReplaceAll(s, prefix+`\`, "~/")
|
||||
s = strings.ReplaceAll(s, prefix+"/", "~/")
|
||||
s = strings.ReplaceAll(s, prefix, "~")
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func Write(b Bundle) (string, error) {
|
||||
|
||||
@@ -3,7 +3,6 @@ package crash
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -11,9 +10,6 @@ import (
|
||||
func TestBundleHidesHome(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Setenv("USERPROFILE", home)
|
||||
}
|
||||
root := filepath.Join(home, "game")
|
||||
dir, err := Write(Bundle{Root: root, Kind: "panic", Where: "panel inspector", Panic: "boom in " + root, Stack: []byte(root + "/x.go:1")})
|
||||
if err != nil {
|
||||
@@ -29,12 +25,3 @@ func TestBundleHidesHome(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHideHomeWithWindowsSeparators(t *testing.T) {
|
||||
home := `C:\Users\Alice`
|
||||
for _, input := range []string{`C:\Users\Alice\game\main.go`, `C:/Users/Alice/game/main.go`} {
|
||||
if got := hideHome(input, home); got != `~/game/main.go` && got != `~/game\main.go` {
|
||||
t.Fatalf("hideHome(%q) = %q", input, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,99 @@
|
||||
package docstool
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStudioWebPackageVersionsEveryBridgeResource(t *testing.T) {
|
||||
web := filepath.Join("..", "..", "tools", "artstudio", "web")
|
||||
dir := t.TempDir()
|
||||
wasm := filepath.Join(dir, "artstudio.wasm")
|
||||
if err := os.WriteFile(wasm, []byte("wasm-build-one"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
support := []byte("versioned go wasm support")
|
||||
check := func() string {
|
||||
t.Helper()
|
||||
if err := packageStudioWeb(web, dir, support); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
index, err := os.ReadFile(filepath.Join(dir, "index.html"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
js, err := os.ReadFile(filepath.Join(dir, "studio.js"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
match := regexp.MustCompile(`studio\.css\?v=([a-f0-9]{10})`).FindSubmatch(index)
|
||||
if len(match) != 2 {
|
||||
t.Fatalf("no versioned stylesheet: %s", index)
|
||||
}
|
||||
version := string(match[1])
|
||||
for _, item := range []string{"wasm_exec.js", "recovery.js", "studio.js"} {
|
||||
if !strings.Contains(string(index), item+"?v="+version) {
|
||||
t.Fatalf("%s is not bound to %s", item, version)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(string(js), "fetch(window.AegoStudioRelease?.wasmURL || 'artstudio.wasm?v="+version+"')") {
|
||||
t.Fatal("WASM fetch lost the release override or the versioned local fallback")
|
||||
}
|
||||
actual, err := os.ReadFile(filepath.Join(dir, "wasm_exec.js"))
|
||||
if err != nil || string(actual) != string(support) {
|
||||
t.Fatal("WASM support changed")
|
||||
}
|
||||
return version
|
||||
}
|
||||
first := check()
|
||||
if second := check(); first != second {
|
||||
t.Fatalf("same inputs are not deterministic: %s != %s", first, second)
|
||||
}
|
||||
if err := os.WriteFile(wasm, []byte("wasm-build-two"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if next := check(); next == first {
|
||||
t.Fatal("changed WASM reused the old bridge version")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStudioWebPackageRejectsUnknownEntryPoints(t *testing.T) {
|
||||
web, dir := t.TempDir(), t.TempDir()
|
||||
for name, contents := range map[string]string{"index.html": "unexpected index", "studio.css": "css", "studio.js": "fetch('artstudio.wasm')", "recovery.js": "js"} {
|
||||
if err := os.WriteFile(filepath.Join(web, name), []byte(contents), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "artstudio.wasm"), []byte("wasm"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := packageStudioWeb(web, dir, []byte("support")); err == nil {
|
||||
t.Fatal("unknown index was silently packaged")
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, "index.html")); !os.IsNotExist(err) {
|
||||
t.Fatalf("invalid index was published: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStudioWebPackageRejectsAmbiguousWASMFetch(t *testing.T) {
|
||||
web, dir := t.TempDir(), t.TempDir()
|
||||
index := `<link href="studio.css"><script src="wasm_exec.js"></script><script src="recovery.js"></script><script src="studio.js"></script>`
|
||||
fetch := `fetch(window.AegoStudioRelease?.wasmURL || 'artstudio.wasm')`
|
||||
for name, contents := range map[string]string{"index.html": index, "studio.css": "css", "studio.js": fetch + ";" + fetch, "recovery.js": "js"} {
|
||||
if err := os.WriteFile(filepath.Join(web, name), []byte(contents), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "artstudio.wasm"), []byte("wasm"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := packageStudioWeb(web, dir, []byte("support")); err == nil {
|
||||
t.Fatal("ambiguous WASM fetch was silently packaged")
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, "index.html")); !os.IsNotExist(err) {
|
||||
t.Fatalf("ambiguous page was published: %v", err)
|
||||
}
|
||||
}
|
||||
+42
-18
@@ -3,7 +3,9 @@
|
||||
package docsview
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
playmods "aego/app/assembly/playground"
|
||||
@@ -15,26 +17,29 @@ import (
|
||||
)
|
||||
|
||||
type request struct {
|
||||
Scene string `json:"scene"`
|
||||
Select string `json:"select"`
|
||||
Code string `json:"code"`
|
||||
Prefabs map[string]string `json:"prefabs"`
|
||||
Artifacts []byte `json:"artifacts"`
|
||||
Component string `json:"component"`
|
||||
Field string `json:"field"`
|
||||
Value float64 `json:"value"`
|
||||
Node string `json:"node"`
|
||||
Theme string `json:"theme"`
|
||||
Lang string `json:"lang"`
|
||||
Scene string `json:"scene"`
|
||||
Select string `json:"select"`
|
||||
Code string `json:"code"`
|
||||
Prefabs map[string]string `json:"prefabs"`
|
||||
Artifacts []byte `json:"artifacts"`
|
||||
Component string `json:"component"`
|
||||
Field string `json:"field"`
|
||||
Value json.RawMessage `json:"value"`
|
||||
Fields []string `json:"fields"`
|
||||
Dependencies []string `json:"dependencies"`
|
||||
Node string `json:"node"`
|
||||
Theme string `json:"theme"`
|
||||
Lang string `json:"lang"`
|
||||
}
|
||||
|
||||
type Reply struct {
|
||||
Text string `json:"text,omitempty"`
|
||||
Values map[string]string `json:"values,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
State *docsession.State `json:"state,omitempty"`
|
||||
Log []Line `json:"log,omitempty"`
|
||||
Pose []float64 `json:"pose,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Values map[string]string `json:"values,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
State *docsession.State `json:"state,omitempty"`
|
||||
Log []Line `json:"log,omitempty"`
|
||||
Pose []float64 `json:"pose,omitempty"`
|
||||
Inspection *Inspection `json:"inspection,omitempty"`
|
||||
}
|
||||
|
||||
func (a *App) Call(method string, payload []byte) Reply {
|
||||
@@ -77,7 +82,20 @@ func (a *App) Call(method string, payload []byte) Reply {
|
||||
case "redo":
|
||||
err = s.Redo()
|
||||
case "set":
|
||||
err = s.Set(req.Component, req.Field, req.Value)
|
||||
value := bytes.TrimSpace(req.Value)
|
||||
if len(value) == 0 || bytes.Equal(value, []byte("null")) {
|
||||
err = fmt.Errorf("set needs a number or string value")
|
||||
} else if value[0] == '"' {
|
||||
var text string
|
||||
if err = json.Unmarshal(value, &text); err == nil {
|
||||
err = s.SetString(req.Component, req.Field, text)
|
||||
}
|
||||
} else {
|
||||
var number float64
|
||||
if err = json.Unmarshal(value, &number); err == nil {
|
||||
err = s.Set(req.Component, req.Field, number)
|
||||
}
|
||||
}
|
||||
case "state":
|
||||
st := s.State()
|
||||
return Reply{State: &st}
|
||||
@@ -92,6 +110,12 @@ func (a *App) Call(method string, payload []byte) Reply {
|
||||
}
|
||||
p := sc.WorldPosition(h)
|
||||
return Reply{Pose: []float64{float64(p.X), float64(p.Y)}}
|
||||
case "inspect":
|
||||
inspection, err := a.inspectTry(req.Component, req.Fields, req.Dependencies)
|
||||
if err != nil {
|
||||
return Reply{Error: err.Error()}
|
||||
}
|
||||
return Reply{Inspection: inspection}
|
||||
case "log":
|
||||
out := Reply{Log: a.lines}
|
||||
a.lines = nil
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,52 @@
|
||||
//go:build aego_editor
|
||||
|
||||
package docsview
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"aego/app/playground"
|
||||
)
|
||||
|
||||
// Component-specific demonstrations must not silently replace the existing
|
||||
// authored scenes and scripts used by the broader guides.
|
||||
func TestComponentTryGuidesPreserveAuthoredScenes(t *testing.T) {
|
||||
p, err := playground.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, spec := range []struct{ guide, component, sum, script string }{
|
||||
{"camera", "camera.CameraFollow2D", "0a7ed8d6fd9cce7ee144cd2a0ca91a6ff377220cc7b737ebe9feb93e10331f3a", ""},
|
||||
{"camera/follow", "camera.CameraFollow2D", "0a7ed8d6fd9cce7ee144cd2a0ca91a6ff377220cc7b737ebe9feb93e10331f3a", ""},
|
||||
{"camera/zones", "camera.CameraZone2D", "9f5b36a8de5584836d466b8437510c1990e3c719d41a4c65dfa591b2b7c634e8", ""},
|
||||
{"camera/outputs", "camera.CameraBrain2D", "d475b935c8c41973653b44aa1ff75d4871263a0ad2cb9d56097f234818be1aa4", ""},
|
||||
{"camera/virtual", "camera.VirtualCamera2D", "14e775168e8f7f5250d42970db4578d39219a9e3bb9d0dab839892fd92ae88a8", "af3febb779542a8605bd2899667e87d5cf81826ca4d15efcbab85a0c0efcffab"},
|
||||
{"physics", "physics.Body2D", "87081fb3f66fb919ef02833f30d052457b456c841b2b6406c9930ad47884644c", ""},
|
||||
} {
|
||||
t.Run(spec.guide, func(t *testing.T) {
|
||||
guide, ok := p.Docs().Get(spec.guide)
|
||||
if !ok {
|
||||
t.Fatal("guide not found")
|
||||
}
|
||||
if got := fmt.Sprintf("%x", sha256.Sum256([]byte(guide.Try))); got != spec.sum || guide.TrySelect != spec.component {
|
||||
t.Fatalf("authored guide scene or selection changed: sha=%s select=%s", got, guide.TrySelect)
|
||||
}
|
||||
if spec.script != "" {
|
||||
if got := fmt.Sprintf("%x", sha256.Sum256([]byte(guide.TryScript))); got != spec.script {
|
||||
t.Fatalf("authored guide script changed: sha=%s", got)
|
||||
}
|
||||
}
|
||||
component, ok := p.Docs().Get(spec.component)
|
||||
if !ok || component.Demo == nil || component.Try == guide.Try {
|
||||
t.Fatalf("%s needs its distinct executable component demo", spec.component)
|
||||
}
|
||||
})
|
||||
}
|
||||
for _, problem := range p.Docs().Problems() {
|
||||
if problem.Error {
|
||||
t.Fatalf("documentation registry error: %+v", problem)
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,72 @@
|
||||
//go:build aego_editor
|
||||
|
||||
package docsview
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"aego/app/playground"
|
||||
)
|
||||
|
||||
func TestTryInspectionReadsSelectedRunningComponent(t *testing.T) {
|
||||
p, err := playground.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
e, ok := p.Docs().Get("engine.SpriteRenderer")
|
||||
if !ok || e.Try == "" {
|
||||
t.Fatal("missing SpriteRenderer Try")
|
||||
}
|
||||
a := newHeadless(t)
|
||||
load, _ := json.Marshal(map[string]any{"scene": e.Try, "select": e.TrySelect, "artifacts": e.TryArtifacts})
|
||||
if r := a.Call("load", load); r.Error != "" {
|
||||
t.Fatal(r.Error)
|
||||
}
|
||||
if err := a.Step(10); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req := []byte(`{"component":"engine.SpriteRenderer","fields":["engine.SpriteRenderer.Order"],"dependencies":["ae900000000000000000000000000010"]}`)
|
||||
inspect := func(want float64) {
|
||||
t.Helper()
|
||||
r := a.Call("inspect", req)
|
||||
if r.Error != "" || r.Inspection == nil || !r.Inspection.RuntimeExecuting || r.Inspection.Observables["engine.SpriteRenderer.Order"] != int64(want) || len(r.Inspection.Dependencies) != 1 {
|
||||
t.Fatalf("inspect: %+v", r)
|
||||
}
|
||||
wire, err := json.Marshal(r)
|
||||
if err != nil || !strings.Contains(string(wire), `"engine.SpriteRenderer.Order":`) {
|
||||
t.Fatalf("inspection is not accessible over the real bridge: %s, %v", wire, err)
|
||||
}
|
||||
}
|
||||
inspect(1)
|
||||
before := string(a.session.Text())
|
||||
inspect(1)
|
||||
if string(a.session.Text()) != before || a.session.State().CanUndo {
|
||||
t.Fatal("read-only inspection changed the source document")
|
||||
}
|
||||
if r := a.Call("set", []byte(`{"component":"engine.SpriteRenderer","field":"order","value":-1}`)); r.Error != "" {
|
||||
t.Fatal(r.Error)
|
||||
}
|
||||
if err := a.Step(4); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
inspect(-1)
|
||||
if r := a.Call("reset", nil); r.Error != "" {
|
||||
t.Fatal(r.Error)
|
||||
}
|
||||
if err := a.Step(4); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
inspect(1)
|
||||
for _, invalid := range []string{
|
||||
`{"component":"engine.SpriteRenderer","fields":["engine.SpriteRenderer.NoSuchField"]}`,
|
||||
`{"component":"engine.SpriteRenderer","dependencies":["ffffffffffffffffffffffffffffffff"]}`,
|
||||
`{"component":"engine.SpriteRenderer","dependencies":["ae900000000000000000000000000010","ae900000000000000000000000000010"]}`,
|
||||
`{"component":"engine.Mesh2D","fields":["engine.Mesh2D.Order"]}`,
|
||||
} {
|
||||
if r := a.Call("inspect", []byte(invalid)); r.Error == "" {
|
||||
t.Fatalf("accepted invalid inspection %s: %+v", invalid, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,84 @@
|
||||
//go:build aego_editor
|
||||
|
||||
package docsview
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"aego/app/playground"
|
||||
"aego/asset"
|
||||
"aego/engine/docs"
|
||||
"aego/modules/render2d"
|
||||
"aego/scene"
|
||||
)
|
||||
|
||||
// This executes the real docs Try session with its existing loader, editor
|
||||
// mutation and reset. A headless run does not prove GPU or browser rendering.
|
||||
func TestMesh2DTryLoadsEditsAndResets(t *testing.T) {
|
||||
p, err := playground.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var entry docs.Entry
|
||||
for _, candidate := range p.TryScenes() {
|
||||
if candidate.ID == "engine.Mesh2D" {
|
||||
entry = candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
if entry.Try == "" || entry.TrySelect != "engine.Mesh2D" {
|
||||
t.Fatal("the Mesh2D component does not own a selectable Try scene")
|
||||
}
|
||||
a := newHeadless(t)
|
||||
request, err := json.Marshal(map[string]any{"scene": entry.Try, "select": entry.TrySelect, "artifacts": entry.TryArtifacts})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if reply := a.Call("load", request); reply.Error != "" {
|
||||
t.Fatal(reply.Error)
|
||||
}
|
||||
if err := a.Step(10); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := a.loader.Mesh(asset.BuiltinCube); !ok {
|
||||
t.Fatal("the built-in mesh dependency is unavailable")
|
||||
}
|
||||
order := func() int32 {
|
||||
t.Helper()
|
||||
sc := a.session.Controller().Scene()
|
||||
if sc == nil {
|
||||
t.Fatal("Try runtime is not running")
|
||||
}
|
||||
node := sc.Find(sc.Root(), "Mesh 2D")
|
||||
mesh, ok := scene.Get[render2d.Mesh2D](sc, node)
|
||||
if !ok || mesh.Mesh.ID != asset.BuiltinCube {
|
||||
t.Fatal("Try runtime lost the component or its mesh")
|
||||
}
|
||||
return mesh.Order
|
||||
}
|
||||
if got := order(); got != 2 {
|
||||
t.Fatalf("initial mesh order = %d, want 2", got)
|
||||
}
|
||||
if err := a.session.Set("engine.Mesh2D", "order", 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := a.Step(2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := order(); got != 0 {
|
||||
t.Fatalf("edited mesh order = %d, want 0", got)
|
||||
}
|
||||
if err := a.session.Reset(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := a.Step(2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := order(); got != 2 {
|
||||
t.Fatalf("reset mesh order = %d, want 2", got)
|
||||
}
|
||||
if errors := errorsOf(a); len(errors) > 0 {
|
||||
t.Fatalf("Mesh2D Try faults: %v", errors)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
//go:build aego_editor
|
||||
|
||||
package docsview
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"aego/app/playground"
|
||||
)
|
||||
|
||||
func TestStringTryBridgeHistoryAndValidation(t *testing.T) {
|
||||
p, err := playground.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
e, ok := p.Docs().Get("camera.CameraBrain2D")
|
||||
if !ok || e.Try == "" {
|
||||
t.Fatal("missing CameraBrain2D Try")
|
||||
}
|
||||
a := newHeadless(t)
|
||||
load, err := json.Marshal(map[string]any{"scene": e.Try, "select": e.TrySelect, "artifacts": e.TryArtifacts})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r := a.Call("load", load); r.Error != "" {
|
||||
t.Fatal(r.Error)
|
||||
}
|
||||
step := func() {
|
||||
t.Helper()
|
||||
if err := a.Step(4); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
step()
|
||||
inspect := func(want string) {
|
||||
t.Helper()
|
||||
r := a.Call("inspect", []byte(`{"component":"camera.CameraBrain2D","fields":["camera.CameraBrain2D.Output"]}`))
|
||||
if r.Error != "" || r.Inspection == nil || r.Inspection.Observables["camera.CameraBrain2D.Output"] != want {
|
||||
t.Fatalf("want output %q: %+v", want, r)
|
||||
}
|
||||
}
|
||||
inspect("map")
|
||||
for _, value := range []string{`null`, `true`, `{}`, `[]`, `123`} {
|
||||
r := a.Call("set", []byte(fmt.Sprintf(`{"component":"camera.CameraBrain2D","field":"Output","value":%s}`, value)))
|
||||
if r.Error == "" {
|
||||
t.Fatalf("accepted %s for string field", value)
|
||||
}
|
||||
}
|
||||
for _, invalid := range []string{
|
||||
`{"component":"camera.CameraBrain2D","field":"Output"}`,
|
||||
`{"component":"engine.Camera2D","field":"Zoom","value":"40"}`,
|
||||
} {
|
||||
if r := a.Call("set", []byte(invalid)); r.Error == "" {
|
||||
t.Fatalf("accepted invalid set: %s", invalid)
|
||||
}
|
||||
}
|
||||
inspect("map")
|
||||
if a.session.State().CanUndo {
|
||||
t.Fatal("rejected edits changed history")
|
||||
}
|
||||
set := func(value string) {
|
||||
t.Helper()
|
||||
data, err := json.Marshal(map[string]any{"component": "camera.CameraBrain2D", "field": "Output", "value": value})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r := a.Call("set", data); r.Error != "" {
|
||||
t.Fatal(r.Error)
|
||||
}
|
||||
step()
|
||||
}
|
||||
set("")
|
||||
inspect("")
|
||||
if r := a.Call("undo", nil); r.Error != "" {
|
||||
t.Fatal(r.Error)
|
||||
}
|
||||
step()
|
||||
inspect("map")
|
||||
if r := a.Call("redo", nil); r.Error != "" {
|
||||
t.Fatal(r.Error)
|
||||
}
|
||||
step()
|
||||
inspect("")
|
||||
set("карта \"A\"")
|
||||
inspect("карта \"A\"")
|
||||
if r := a.Call("reset", nil); r.Error != "" {
|
||||
t.Fatal(r.Error)
|
||||
}
|
||||
step()
|
||||
inspect("map")
|
||||
if a.session.State().CanUndo {
|
||||
t.Fatal("reset retained history")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
//go:build aego_editor && aego_gltest && linux && cgo && !aego_nonative
|
||||
|
||||
package docsview
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestBrowserProcessDeadlineFailsClosed(t *testing.T) {
|
||||
const runner = `
|
||||
const readline = require('node:readline');
|
||||
readline.createInterface({input: process.stdin}).on('line', line => {
|
||||
const req = JSON.parse(line);
|
||||
if (req.method === 'hang') return;
|
||||
const id = req.method === 'wrong-id' ? req.id + 1 : req.id;
|
||||
const error = req.method === 'error' ? 'deliberate failure' : undefined;
|
||||
process.stdout.write(JSON.stringify({id, error, result: {ok: true}}) + '\n');
|
||||
});`
|
||||
file := filepath.Join(t.TempDir(), "runner.cjs")
|
||||
if err := os.WriteFile(file, []byte(runner), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p, err := startBrowserProcess(context.Background(), t.TempDir(), file)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer p.close()
|
||||
var reply struct {
|
||||
OK bool `json:"ok"`
|
||||
}
|
||||
if err := p.call(context.Background(), "ok", nil, &reply); err != nil || !reply.OK {
|
||||
t.Fatalf("valid browser reply failed: %+v, %v", reply, err)
|
||||
}
|
||||
if err := p.call(context.Background(), "wrong-id", nil, nil); err == nil || !strings.Contains(err.Error(), "ID mismatch") {
|
||||
t.Fatalf("wrong response ID accepted: %v", err)
|
||||
}
|
||||
if err := p.call(context.Background(), "error", nil, nil); err == nil || !strings.Contains(err.Error(), "deliberate failure") {
|
||||
t.Fatalf("browser error accepted: %v", err)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
|
||||
defer cancel()
|
||||
start := time.Now()
|
||||
if err := p.call(ctx, "hang", nil, nil); !errors.Is(err, context.DeadlineExceeded) {
|
||||
t.Fatalf("hung browser must fail with context deadline, got %v", err)
|
||||
}
|
||||
if elapsed := time.Since(start); elapsed > 5*time.Second {
|
||||
t.Fatalf("hung browser did not stop promptly: %v", elapsed)
|
||||
}
|
||||
if err := p.call(context.Background(), "ok", nil, &reply); err == nil {
|
||||
t.Fatal("a killed runner accepted a later probe")
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -109,7 +109,7 @@ const GuideExample = `tx version=1 { label="Add Ledge" op "add-node" { name="Led
|
||||
// Guide is the system prompt the editor adds for the model.
|
||||
const Guide = "You work inside the Aego editor. Write tools wait until the user applies or rejects the change; a rejected call means the user said no, so ask before trying it again. Edits to open scenes become Undo steps in the editor and are saved by the user. " +
|
||||
"Each write call is one card the user has to apply, so send related edits of one scene as one scene_apply transaction: a new node with its components and fields is one call, not a chain of add-node, add-comp and set. Give the transaction a short label; it names the Undo step. Example tx: " + GuideExample + ". Use the single-step tools for one small change. For an open document use editor_save_document, editor_undo or editor_redo with its project path; these ask the user and never act on a different document. " +
|
||||
"Read the selected Console entry as evidence. Read .aeshader and .aemat buffers with editor_material_source by AssetID or project path. Read scripts with script_read; change one with script_edit for a few lines or script_write for the whole text, and add one with script_create. A script attaches to a node like a component: add-comp with its type name (EnemyAI or the full mygame.enemy.EnemyAI), and its exported fields are read and set like component fields (EnemyAI.PointA). Node reference fields take a node path such as Root/PatrolA1. " +
|
||||
"Read the selected Console entry as evidence. Read .aeshader and .aemat buffers with editor_material_source by AssetID or project path. For an editable .aeart use art_read and art_capture to inspect the current unsaved document; use its exact revision with art_apply to propose one undoable batch. Read scripts with script_read; change one with script_edit for a few lines or script_write for the whole text, and add one with script_create. A script attaches to a node like a component: add-comp with its type name (EnemyAI or the full mygame.enemy.EnemyAI), and its exported fields are read and set like component fields (EnemyAI.PointA). Node reference fields take a node path such as Root/PatrolA1. " +
|
||||
"Check behaviour with scene_run: it plays the scene as it is in the editor; input presses actions by frame (30 press Jump; 10-60 hold Right; 0-40 axis Move 1), assert checks values at a frame or at any frame (@120 Player.transform.position.y < 3; EnemyLeft.Health.hp == 0; exists Ledge) and fails the run when one does not hold, watch reports node positions, shot captures up to 3 frames as images. If a check fails, inspect its result and change the test or fix the cause; do not repeatedly run an unchanged failing test. A sprite with no texture yet is drawn, not built from colored rectangles: asset_pixels takes a character grid and a palette (frames give an atlas and a flipbook), asset_svg smooth shapes for tiles, UI and icons; both answer with a preview to check. Draw in the style of project.aego's art section (project_get art): its palette, and without a palette argument 0-9 and a-z are its colors. When the user has to choose between ways to do something, call ask_user with the options instead of listing them in your answer."
|
||||
|
||||
// Start opens the MCP server on loopback; tools run on the editor's main thread through OnMain.
|
||||
|
||||
@@ -40,6 +40,7 @@ type undoRef struct {
|
||||
depth int
|
||||
script *scriptUndo
|
||||
drawing *drawingCommand
|
||||
art *artUndo
|
||||
}
|
||||
|
||||
// Proposal is one write call from the model, previewed as a dry run and waiting for the user (D342).
|
||||
@@ -145,7 +146,14 @@ func (b *Bridge) proposeVia(c *agent.Command, a agent.Args, via string, direct b
|
||||
func (b *Bridge) preview(c *agent.Command, a agent.Args, via string, direct bool) (*Proposal, error) {
|
||||
dry := maps.Clone(a)
|
||||
dry["dry-run"] = true
|
||||
out, err := agent.Run(b.sess, c.Name, dry)
|
||||
// Prepared transactions retain their history label from preview through
|
||||
// approval, so record the external origin before constructing them.
|
||||
out, err := func() (*agent.Output, error) {
|
||||
previous := b.via
|
||||
b.via = via
|
||||
defer func() { b.via = previous }()
|
||||
return agent.Run(b.sess, c.Name, dry)
|
||||
}()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -185,7 +193,10 @@ func (b *Bridge) apply(p *Proposal) (*agent.Output, error) {
|
||||
defer func() { b.via = "" }()
|
||||
var out *agent.Output
|
||||
var err error
|
||||
if p.Preview.Drawing != nil {
|
||||
if p.Preview.Art != nil {
|
||||
out, err = p.Preview.Art.Commit()
|
||||
p.Preview.Art = nil
|
||||
} else if p.Preview.Drawing != nil {
|
||||
out, err = b.applyDrawing(p)
|
||||
} else {
|
||||
out, err = b.run(p.Command, p.Args)
|
||||
@@ -211,6 +222,9 @@ func (b *Bridge) await(p *Proposal) (bool, error) {
|
||||
if e := b.main(func() {
|
||||
if p.State == Pending {
|
||||
p.State, expired = Expired, true
|
||||
if p.Preview != nil {
|
||||
p.Preview.Art = nil
|
||||
}
|
||||
}
|
||||
}); e != nil {
|
||||
return false, e
|
||||
@@ -254,6 +268,8 @@ func (b *Bridge) Decide(id int, apply bool) bool {
|
||||
p.State = Rejected
|
||||
if apply {
|
||||
p.State = Approved
|
||||
} else if p.Preview != nil {
|
||||
p.Preview.Art = nil
|
||||
}
|
||||
p.decide <- apply
|
||||
return true
|
||||
@@ -270,6 +286,9 @@ func (b *Bridge) RejectAll() {
|
||||
for _, p := range b.proposals {
|
||||
if p.generation != 0 && p.State == Approved {
|
||||
p.State, p.Err = Rejected, errCancelled.Error()
|
||||
if p.Preview != nil {
|
||||
p.Preview.Art = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
b.dismissQuestions()
|
||||
@@ -311,6 +330,9 @@ func (b *Bridge) CanUndo(id int) bool {
|
||||
if p == nil || p.undo == nil || p.State != Applied {
|
||||
return false
|
||||
}
|
||||
if p.undo.art != nil {
|
||||
return b.canUndoArt(p.undo.art)
|
||||
}
|
||||
if p.undo.drawing != nil {
|
||||
if p.undo.drawing.check(true) != nil {
|
||||
return false
|
||||
@@ -335,6 +357,13 @@ func (b *Bridge) Undo(id int) error {
|
||||
return errors.New("this change is no longer the last step; undo it with Edit → Undo")
|
||||
}
|
||||
p := b.find(id)
|
||||
if p.undo.art != nil {
|
||||
if err := b.undoArt(p.undo.art); err != nil {
|
||||
return err
|
||||
}
|
||||
p.State = Undone
|
||||
return nil
|
||||
}
|
||||
if p.undo.drawing != nil && p.undo.doc == 0 {
|
||||
if err := p.undo.drawing.Undo(nil); err != nil {
|
||||
return err
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -159,8 +159,8 @@ func TestDiagnosticComposerConsoleProfilerNative(t *testing.T) {
|
||||
t.Fatal("SDL clipboard rejected window screenshot")
|
||||
}
|
||||
key(input.KeyV, true, false)
|
||||
if panel.Input() != "draft line" {
|
||||
t.Fatalf("screenshot paste changed text: %q", panel.Input())
|
||||
if panel.Input() != "draft line[Image 1]" {
|
||||
t.Fatalf("screenshot paste lost draft or marker: %q", panel.Input())
|
||||
}
|
||||
key(input.KeyA, true, false)
|
||||
long := "line 01\nline 02\nline 03\nline 04\nline 05\nline 06\nline 07\nline 08\nlast visible line"
|
||||
@@ -169,6 +169,19 @@ func TestDiagnosticComposerConsoleProfilerNative(t *testing.T) {
|
||||
if panel.Input() != long {
|
||||
t.Fatalf("text not editable after clipboard image: %q", panel.Input())
|
||||
}
|
||||
// Replacing the marker removes its attachment. Reattach the screenshot so
|
||||
// the remainder also exercises thumbnail layout and the unsent draft.
|
||||
if thumbs := s.App.UIList().TestDrawnItems(screen, ed.UI.Font(), "", 512, height*512/width); len(thumbs) != 0 {
|
||||
t.Fatalf("replacing image marker left an orphan attachment: %+v", thumbs)
|
||||
}
|
||||
if !ed.Clipboard.SetData(clip.MimePNG, original.Bytes(), "") {
|
||||
t.Fatal("SDL clipboard rejected screenshot on reattach")
|
||||
}
|
||||
key(input.KeyV, true, false)
|
||||
long += "[Image 1]"
|
||||
if panel.Input() != long {
|
||||
t.Fatalf("reattaching screenshot lost multiline draft or marker: %q", panel.Input())
|
||||
}
|
||||
last := text("last visible line")
|
||||
if !inside(last) || last.Clip.H < small.H+3*ed.UI.LineHeight() {
|
||||
t.Fatalf("multiline did not grow: one=%+v multiline=%+v", small, last)
|
||||
|
||||
+20
-8
@@ -52,6 +52,9 @@ type Config struct {
|
||||
OpenPath string
|
||||
NativeNote string
|
||||
LauncherTab string
|
||||
// RemoteProject is set by the confirmed remote-example launcher. It is
|
||||
// retained for the session, even if project.aego changes after startup.
|
||||
RemoteProject bool
|
||||
// AgentBackend overrides the saved provider for tests/replay; headless nil means no panel.
|
||||
AgentBackend backend.Backend
|
||||
}
|
||||
@@ -132,6 +135,9 @@ func Run(p *project.Project, cfg Config, mods ...sdk.Module) error {
|
||||
}
|
||||
|
||||
func Open(p *project.Project, cfg Config, mods ...sdk.Module) (*Session, error) {
|
||||
if cfg.RemoteProject && p.Native {
|
||||
return nil, errors.New(i18n.Tr("launcher.remote.native_blocked", "Remote examples cannot start a native Go project."))
|
||||
}
|
||||
platform.SetAppMetadata(platform.AppMetadata{Name: "Aego Editor", Version: engine.Version, Identifier: "ru.akarpov.aego.editor"})
|
||||
if cfg.Width <= 0 {
|
||||
cfg.Width = 1600
|
||||
@@ -155,14 +161,15 @@ func Open(p *project.Project, cfg Config, mods ...sdk.Module) (*Session, error)
|
||||
AppID: p.Module + ".editor",
|
||||
Trace: true,
|
||||
},
|
||||
Logger: cfg.Logger,
|
||||
Debug: cfg.Debug,
|
||||
LoopScope: engine.ScopeFrame,
|
||||
LocateBindings: true,
|
||||
Docs: true,
|
||||
Editor: true,
|
||||
Catalog: cat,
|
||||
Profile: cfg.Profile,
|
||||
Logger: cfg.Logger,
|
||||
Debug: cfg.Debug,
|
||||
LoopScope: engine.ScopeFrame,
|
||||
LocateBindings: true,
|
||||
Docs: true,
|
||||
Editor: true,
|
||||
Catalog: cat,
|
||||
Profile: cfg.Profile,
|
||||
NoExternalImportTools: cfg.RemoteProject,
|
||||
}, sdk.AdaptAll(mods)...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -242,6 +249,11 @@ func Open(p *project.Project, cfg Config, mods ...sdk.Module) (*Session, error)
|
||||
dirty = append(dirty, doc.Path)
|
||||
}
|
||||
}
|
||||
for _, doc := range s.App.Context().Docs.AssetDocuments() {
|
||||
if doc.Binding.Dirty != nil && doc.Binding.Dirty() {
|
||||
dirty = append(dirty, doc.Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
return dirty
|
||||
}),
|
||||
|
||||
+30
-1
@@ -16,6 +16,7 @@ import (
|
||||
"aego/app/updates"
|
||||
"aego/editor/core/session"
|
||||
"aego/editor/shell/launcher"
|
||||
"aego/engine/i18n"
|
||||
"aego/platform"
|
||||
"aego/project"
|
||||
"aego/sdk"
|
||||
@@ -153,11 +154,15 @@ func Start(e Entry) error {
|
||||
dir, file := resolveArg(e.Arg)
|
||||
if e.Launcher != "" || (dir == "" && !hasProject(".")) {
|
||||
cfg.LauncherTab = e.Launcher
|
||||
root, err := Launch(cfg)
|
||||
root, entry, remote, err := launchWithEntry(cfg)
|
||||
if err != nil || root == "" {
|
||||
return err
|
||||
}
|
||||
dir, file = root, ""
|
||||
if remote {
|
||||
cfg.OpenPath = entry
|
||||
cfg.RemoteProject = true
|
||||
}
|
||||
}
|
||||
if dir == "" {
|
||||
dir = "."
|
||||
@@ -166,10 +171,34 @@ func Start(e Entry) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
copy, managed, err := (launcherRemote{}).existingProject(root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if managed {
|
||||
cfg.RemoteProject = true
|
||||
if file != "" {
|
||||
file, err = filepath.EvalSymlinks(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
root = copy.Directory
|
||||
if file == "" && cfg.OpenPath == "" {
|
||||
entry, ok := relInside(root, copy.Entry)
|
||||
if !ok {
|
||||
return errors.New("remote example entry is outside its project")
|
||||
}
|
||||
cfg.OpenPath = entry
|
||||
}
|
||||
}
|
||||
p, err := project.Load(root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cfg.RemoteProject && p.Native {
|
||||
return errors.New(i18n.Tr("launcher.remote.native_blocked", "Remote examples cannot start a native Go project."))
|
||||
}
|
||||
if file != "" {
|
||||
if rel, ok := relInside(p.Root, file); ok {
|
||||
cfg.OpenPath = rel
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ func (x *exporter) Prepare() any {
|
||||
if x.dirtySources != nil {
|
||||
for _, rel := range x.dirtySources() {
|
||||
switch strings.ToLower(filepath.Ext(rel)) {
|
||||
case ".aemat", ".aeshader", ".glsl", ".vert", ".frag", ".meta", ".aepreset":
|
||||
case ".aemat", ".aeshader", ".glsl", ".vert", ".frag", ".meta", ".aepreset", ".aeart":
|
||||
snapshot.DirtySources = append(snapshot.DirtySources, rel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ func TestExportRejectsUnsavedMetadataWorkingOverlay(t *testing.T) {
|
||||
}
|
||||
defer pl.Close()
|
||||
// The loader may display a private Undo generation while the pipeline retains the saved artifact; export must reject this even for .meta changes.
|
||||
for _, path := range []string{"assets/platform_height.png.meta", "assets/effects/Dust.aepreset"} {
|
||||
for _, path := range []string{"assets/platform_height.png.meta", "assets/effects/Dust.aepreset", "assets/hero.aeart"} {
|
||||
t.Run(path, func(t *testing.T) {
|
||||
x := &exporter{p: p, pl: pl, dirtySources: func() []string { return []string{path} }}
|
||||
snapshot := x.Prepare().(*gameexport.Snapshot)
|
||||
|
||||
+17
-4
@@ -242,6 +242,7 @@ func OpenLauncher(cfg Config) (*LaunchSession, error) {
|
||||
RecentPath: cfg.Recent,
|
||||
Home: projectsHome(),
|
||||
Image: d.image,
|
||||
Remote: launcherRemote{},
|
||||
Theme: func() theme.Mode { return d.prefs.Theme },
|
||||
SetTheme: d.setTheme,
|
||||
Language: func() string { return d.prefs.Language },
|
||||
@@ -285,24 +286,36 @@ func (ls *LaunchSession) Step() bool {
|
||||
}
|
||||
|
||||
func (ls *LaunchSession) Close() {
|
||||
if ls.UI != nil {
|
||||
ls.UI.Close()
|
||||
}
|
||||
ls.Engine.Close()
|
||||
}
|
||||
|
||||
func Launch(cfg Config) (string, error) {
|
||||
root, _, remote, err := launchWithEntry(cfg)
|
||||
if err == nil && remote {
|
||||
return "", errors.New("remote examples require the entry-aware editor launch path")
|
||||
}
|
||||
return root, err
|
||||
}
|
||||
|
||||
func launchWithEntry(cfg Config) (root, entry string, remote bool, err error) {
|
||||
ls, err := OpenLauncher(cfg)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", "", false, err
|
||||
}
|
||||
if cfg.Frames > 0 {
|
||||
for i := 0; i < cfg.Frames && ls.Step(); i++ {
|
||||
}
|
||||
} else if err := ls.Engine.Run(ls.driver); err != nil {
|
||||
ls.Close()
|
||||
return "", err
|
||||
return "", "", false, err
|
||||
}
|
||||
ls.Close()
|
||||
root, _ := ls.UI.Result()
|
||||
return root, nil
|
||||
root, _ = ls.UI.Result()
|
||||
entry, remote = ls.UI.ResultEntry()
|
||||
return root, entry, remote, nil
|
||||
}
|
||||
|
||||
func touchRecent(cfg Config, root, name string, lg *log.ChannelLogger) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,84 +0,0 @@
|
||||
//go:build aego_gltest
|
||||
|
||||
package edit
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"aego/asset"
|
||||
"aego/core/log"
|
||||
"aego/core/mathx"
|
||||
"aego/editor/core/document"
|
||||
"aego/modules/render2d"
|
||||
"aego/scene"
|
||||
"aego/scene/frame"
|
||||
)
|
||||
|
||||
func TestSceneView3DShows2DSpritePixels(t *testing.T) {
|
||||
runtime.LockOSThread()
|
||||
defer runtime.UnlockOSThread()
|
||||
p := newProject(t)
|
||||
const path = "scenes/2d-in-3d.aescene"
|
||||
writeScene(t, p, path, func(s *scene.Scene) {
|
||||
h := s.Create(s.Root(), "Red sprite")
|
||||
sr := scene.Add[render2d.SpriteRenderer](s, h)
|
||||
*sr = render2d.DefaultSpriteRenderer()
|
||||
sr.Sprite = asset.NewRef[asset.Texture](asset.BuiltinWhite)
|
||||
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.Mode = document.View3D
|
||||
doc.View.Orbit = document.Orbit{Distance: 10, FOV: 60}
|
||||
ed.Settings.Grid = false
|
||||
for range 3 {
|
||||
s.Step()
|
||||
}
|
||||
raw, ok := s.App.SceneView().LightingWorld().ServiceAny(frame.ServiceID)
|
||||
if !ok {
|
||||
t.Fatal("missing Scene View runner")
|
||||
}
|
||||
runner := raw.(*frame.Runner)
|
||||
readRed := func() int {
|
||||
t.Helper()
|
||||
for range 2 {
|
||||
s.Step()
|
||||
}
|
||||
target, w, h := runner.Target()
|
||||
pixels := make([]byte, w*h*4)
|
||||
if target.IsZero() {
|
||||
t.Fatal("no Scene View target")
|
||||
}
|
||||
if err := ed.Renderer.Device().ReadPixels(target, 0, 0, w, h, pixels); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
count := 0
|
||||
for i := 0; i+3 < len(pixels); i += 4 {
|
||||
if pixels[i] > 160 && pixels[i+1] < 80 && pixels[i+2] < 80 {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
visible := readRed()
|
||||
if visible < 100 {
|
||||
t.Fatalf("2D sprite produced only %d red pixels in 3D Scene View", visible)
|
||||
}
|
||||
sc := doc.Scene()
|
||||
h := sc.Find(sc.Root(), "Red sprite")
|
||||
sc.SetEnabled(h, false)
|
||||
if hidden := readRed(); hidden >= visible/2 {
|
||||
t.Fatalf("disabling the sprite left %d of %d red pixels", hidden, visible)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user