67 Commits
Author SHA1 Message Date
sanspie 9da9851538 docs: description and Open Graph title on every page and the playground
ci / checks (push) Successful in 14m32s
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 17m14s
ci / release (push) Successful in 19m26s
2026-10-21 19:20:52 +03:00
sanspie 4fd1e11741 docs: download buttons for the visitor's OS from the latest release
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 15m54s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 17m37s
ci / release (push) Successful in 19m37s
2026-10-21 19:05:12 +03:00
sanspie 8f9a0299dc depcheck: finish the wasm fixture rename to modules/accessibility
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 19m28s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 17m22s
ci / release (push) Successful in 20m16s
2026-10-21 18:36:14 +03:00
sanspie ca3eedf9b0 docs: tell Dark Reader the site has its own dark theme
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 13m37s
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / release (push) Skipped
ci / checks (push) Failing after 12m24s
2026-10-21 17:17:03 +03:00
sanspie 4f0662eb3b docs: highlighted inline code looks like other inline code and wraps cleanly
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Failing after 5m55s
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 10m29s
ci / release (push) Skipped
2026-10-21 17:12:02 +03:00
sanspie 9abf5c75ab playground: hide the game view for console examples
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci / checks (push) Failing after 10m20s
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci / release (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 12m23s
2026-10-21 17:07:27 +03:00
sanspie 96fef1f6a8 docs: real-export nav golden without the unreleased audio module 2026-10-21 17:06:11 +03:00
sanspie 6230d30620 docs: light code theme, reference icons for script packages and settings, no code ligatures
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Failing after 10m39s
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 11m41s
ci / release (push) Skipped
2026-10-21 17:00:56 +03:00
sanspie 91ea202e74 docs: inline CSS so Firefox paints dark at once, header on the page grid, readable fields table
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 17m28s
ci / checks (push) Successful in 17m7s
ci / release (push) Successful in 27m25s
2026-10-21 16:34:01 +03:00
sanspie 6128c86685 docs: editor icons back, editor theme colors, no white flash or sidebar jump on load
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 16m46s
ci / build (amd64, linux, ubuntu-24.04) (push) Failing after 14m12s
ci / release (push) Skipped
2026-10-21 16:21:51 +03:00
sanspie 2be6ce1d64 docs: self-host IBM Plex Sans and JetBrains Mono
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 14m44s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 16m44s
ci / release (push) Successful in 19m19s
2026-10-21 16:12:07 +03:00
sanspie 2e664ae508 docs: skip image-only lines when picking a page summary
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 15m19s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 17m41s
ci / release (push) Successful in 19m50s
2026-10-21 16:05:34 +03:00
sanspie aab6e0b0c9 docs: quieter site design, flat nav for single-page groups, drawer on mobile, light theme toggle
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci / checks (push) Successful in 16m48s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 18m53s
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / release (push) Successful in 22m38s
2026-10-21 15:57:53 +03:00
sanspie 9ba3e1631f docs: Try it demos for every component, with scripts, prefabs and in-memory saves
ci / release (push) Successful in 36m23s
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 21m16s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 23m8s
2026-10-21 13:11:16 +03:00
sanspie 4068e00375 install: read SHA256SUMS.txt first and fetch the archive by its checksum, past stale proxy caches
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci / checks (push) Successful in 15m44s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 17m13s
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / release (push) Successful in 17m39s
2026-10-21 03:56:04 +03:00
sanspie 914cf39150 release: Linux files and checksums first, keep going when a Windows upload fails
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / release (push) Successful in 20m29s
ci / checks (push) Successful in 14m57s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 16m18s
2026-10-21 02:37:55 +03:00
sanspie dede4d8af3 release: record upload failures in the release body
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 13m53s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 15m0s
ci / release (push) Failing after 15m2s
2026-10-21 02:32:02 +03:00
sanspie b6dd46f523 Stage 10a follow-up: camera scene tests, rewind check, value Adjust
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci / checks (push) Successful in 14m14s
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 16m1s
ci / release (push) Failing after 14m38s
- camera.NewService and SetAccessibility; AttachWorld wires the accessibility settings
- tests/camera: priorities, blends, cuts, zones, follow, bounds, outputs, shake, zero allocations, BrainStep benchmark
- integration: rewind restores the camera state across a zone change
- camera2d.Adjuster passes the view by value (no per-frame allocation), D304 updated
- modules/camera/module.mk: test-camera in CI, bench-camera
2026-10-21 01:32:28 +03:00
sanspie 9d32052624 release: one SHA256SUMS.txt and a zipped Windows installer, the attachment types Gitea accepts
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci / release (push) Failing after 14m32s
ci / checks (push) Successful in 14m14s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 16m8s
2026-10-21 01:31:53 +03:00
sanspie e636801578 release: upload straight to the Gitea server with retries
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / release (push) Failing after 14m22s
ci / checks (push) Successful in 14m30s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 17m14s
2026-10-21 00:29:58 +03:00
sanspie 01fdeef7a8 Aego 0.9.0-beta.1
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 14m2s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 16m23s
ci / release (push) Failing after 14m1s
2026-10-20 23:57:45 +03:00
sanspie 71354f6a60 install scripts: fall back to nightly, explain when nothing is published 2026-10-20 23:55:52 +03:00
sanspie 8ae6832503 edge sync: keep the destination when resuming downloads 2026-10-20 20:11:47 +03:00
sanspie 183d75ed50 gendocs: nav golden without the editor-only tour 2026-10-20 20:07:22 +03:00
sanspie 19edcbf9b2 render3d in the web sandbox without the OBJ importer; 3D try scenes select a shape 2026-10-20 19:49:47 +03:00
sanspie 8ed77a7638 Docs: no favicon request 2026-10-20 19:48:24 +03:00
sanspie ca3c9bf2e7 Docs: 3D Try it scenes, render3d in the sandbox, smarter code language detection 2026-10-20 19:43:38 +03:00
sanspie db59f117a4 deploy: short cache for the root page; edge notes in D303 2026-10-20 19:35:50 +03:00
sanspie 0d2c20da26 Docs: revision in the header, content-versioned assets; deploy passes the commit to the image 2026-10-20 19:31:14 +03:00
sanspie dd41ff15a5 deploy: edge serves the site and wasm from its own copy 2026-10-20 19:12:22 +03:00
sanspie 599e09a35d Docs: Try it on Physics 2D guide pages 2026-10-20 18:26:35 +03:00
sanspie 60614938be Release: Windows installer, install.ps1, component page layout 2026-10-20 18:20:18 +03:00
sanspie a60ca47d31 wip release windows 2026-10-20 18:13:57 +03:00
sanspie 00ae2418a0 Docs: more editor screenshots for the editor, prefab, tags, input, time and physics guides 2026-10-20 17:57:58 +03:00
sanspie 8fc9b4c287 Docs: highlighting for every code block, editor-only tour off the site, page icons, prefetch, field docs for physics, wider Try it, demo camera 2026-10-20 17:50:21 +03:00
sanspie b29934b51d deploy: short HTML cache the edge proxy can store 2026-10-20 17:29:55 +03:00
sanspie c475b42e5d gendocs: Script API index links to type pages; nav golden 2026-10-20 17:01:11 +03:00
sanspie b103816400 websmoke: Try smoke checks the scene tree 2026-10-20 17:00:46 +03:00
sanspie 487170dd9c depcheck: tween in the web sandbox 2026-10-20 16:46:52 +03:00
sanspie 57579356d6 Docs: streamed compile with tee'd progress, tween in the sandbox, console runs step module systems 2026-10-20 16:46:03 +03:00
sanspie d5377410e4 Docs: HTTP cache for the docs runtime, autostart after a first start, cheaper warm loads 2026-10-20 16:29:16 +03:00
sanspie f6c83a6e0c Docs: compile-checked examples for modules outside the sandbox, HasScript fix 2026-10-20 16:03:58 +03:00
sanspie 8d7712d671 Docs: runnable Script API module examples and Run it blocks 2026-10-20 16:03:26 +03:00
sanspie 63980faccb Docs: diagnostics with checked examples 2026-10-20 16:00:13 +03:00
sanspie 0041e7e639 Docs: loader streams compile from the original response 2026-10-20 15:59:50 +03:00
sanspie e334be569f Architecture: D301–D303, amendments for the docs overhaul 2026-10-20 15:49:46 +03:00
sanspie b488958444 Docs panel goldens with short-name links 2026-10-20 15:48:31 +03:00
sanspie 5875a65193 Docs: Try it for every 2D physics component, script examples on component pages, Script API index, smooth wheel scrolling on the web 2026-10-20 15:46:33 +03:00
sanspie 6ab66cb8ae Docs: scene tree, loading progress and cache, release builds, install page, editor screenshots 2026-10-20 15:36:03 +03:00
sanspie f8fe54e9c5 Docs: short-name cross references in the site and the editor docs panel, links from code 2026-10-20 15:17:10 +03:00
sanspie e4326e7e7c Docs: editor icons, editor highlighter, redesigned site and navigation 2026-10-20 15:14:55 +03:00
sanspie 6a22396467 Stage 9 complete
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci / checks (push) Successful in 22m2s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 23m5s
2026-10-20 14:06:22 +03:00
sanspie 21e03128a8 Stage 9.6d2: MCP server and external edits in the editor (D297, D300) 2026-10-20 13:56:45 +03:00
sanspie e5b1964f3e Stage 9.6d1: agent layer and CLI (D299) 2026-10-20 13:48:01 +03:00
sanspie cffa66514b Stage 9.6c2: prefab apply and data editing without the editor (D298)
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 21m2s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 22m8s
2026-10-20 13:05:10 +03:00
sanspie 2744cff481 Stage 9.6c1: project refs and refactorings without the editor (D298)
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 23m31s
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci / checks (push) Successful in 21m57s
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
2026-10-20 12:12:18 +03:00
sanspie 91052a54db make lint: staticcheck the editor build
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 16m26s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 17m48s
2026-10-20 12:06:56 +03:00
sanspie 7f4d87ef2c Stage 9: gameplay toolkit, export and web, agent tooling 2026-10-20 12:06:56 +03:00
sanspie 6cc0673082 CI on master, docs auto-deploy and repo link, macOS build fixes
ci / checks (push) Successful in 13m36s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 14m40s
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
2026-10-16 12:10:43 +03:00
sanspie ab9b003457 Stage 8: physics and module boundaries 2026-10-15 19:16:09 +03:00
sanspie 41d949717d deploy: push-to-deploy docs site and playground
make site-headless builds the CLI without cgo (aego_nonative) and the
static site with the wasm playground. deploy/ holds the image, the
container nginx config (gzip_static with Vary), compose and deploy.sh
run by the post-receive hook on mainserv.

.gitignore ignores only the root site/ output; tools/gendocs/site was
never committed because of it.

Docs site: index and section summaries render code, bold and italic
instead of raw markdown; search.json gets the plain text. Page summaries
drop a leading list or quote marker. The nav toggle is three CSS bars
centred in a 32px button.

Playground: a loading overlay in the editor pane shows a spinner, a
progress bar and the downloaded size while aego.wasm streams in, then
'Starting engine' until the runtime is ready; a failed start keeps the
error on the overlay. docstool writes the wasm size and a version hash
into index.html; play.js, play.css, wasm_exec.js and aego.wasm are
referenced with ?v=<hash> so a deploy never mixes cached old files with
new ones.

Instruction budget: default 1M per callback (was 100k), playground 2M;
the donut needs ~120k per frame. Playground: software 3D donut example,
multi-line log entries render as monospace blocks, a fault is reported
once instead of also as a "script stopped" log line.

The compose project is named aego explicitly; the default name came from
the deploy/ directory and collided with other stacks on the host.
2026-10-14 22:40:20 +03:00
sanspie 69d19e501f Stage 7: documentation, help, playground; templates, aego new/bootstrap/icons, launcher
Documentation: engine/docs assembles one registry from the script
bindings, the schema, module docs and module-owned script paths; problems
are reported as warnings at build time. Module registries record doc
sources and script module owners; diag.Codes lists every diagnostic code.

Project bootstrap: project.defaultSpace ("2d"/"3d"), EnsureDirs creates
the default folders when the editor opens a project (reported in the
console), prefabs/ is a pipeline root. Native builds drop -trimpath so
binding locations resolve inside native/.

Editor files: create scene/prefab/script from menus, move/rename/duplicate
/delete with undo, script package retargeting on move, open scenes and
script panels follow renames, Save As / Rename Scene prompts, scene
picker, startup scene. Scoped shortcuts fall back to the global command
when disabled; Save As is Ctrl+Shift+S, Save All Ctrl+Alt+S.

Script editor: Go to Implementation (stub → native/ source), host
definitions always resolve to stubs, bracket match/mismatch states,
rainbow brackets (persisted), mono code font (ui-mono atlases, per-command
fonts in draw.List, PushFont/PopFont restored by Mark/Restore).

Tests: langsvc definition/implementation, bracket states, dispatch
fallback, script renames, font stack, editor file-op integration, golden
mono glyph and bracket references; depcheck rule for engine/docs.

Docs pipeline: engine, render2d and script/engine ship their own res/docs;
keyword hover text comes from the registry through manifest.Keywords;
project script symbols join the registry; aego docs export|check|build|
serve, --tool docs in native binaries, aego check --explain; app/docstool
and the tools/gendocs module (goldmark) build the static site.

Playground: app/playground + cmd/aego-playground (js/wasm, 3.3 MB gzip)
compile, highlight and run scripts in the browser; run blocks in the
language docs become examples. engine/builtin/headless is the module set
without render3d; workspace.Config.FS; StubFor/StubPaths; backends and the
script importer drop out of js builds.

VM: one frame pointer in the interpreter loop, direct host calls, panic
recovery once per run instead of per host call (Update2k -19%,
Schedule10k -24% against the previous head). bench-build/compare/profile
run pre-built, pinned, interleaved binaries.

Editor: structural bracket levels and current-block highlight, indent
guides, occurrence highlighting with write marks, multi-format clipboard
(copy/cut/paste assets, paste images as sprites or textures). gl33 probes
a renderable depth format and reports framebuffer failures by stage.

7 E:
- project templates empty/2d/2.5d/3d, app/projects as the single creation path (D148)
- launcher and recent projects (D149)
- orthographic Camera3D and Scene View (D150)
- reference states in the Inspector, ids.Short (D151)
- sceneio remaps node references when loading without KeepIDs (D152)
- exported ComponentRef<T> (D153)
- aego icons via module.Audit (D146)
2026-10-13 01:08:56 +03:00
sanspie cf68c71f05 Stage 6: coroutines, signals, ScriptRef, workspace CLI, editor tooling
Language: yield/signal/coroutine become keywords, start is contextual.
Coroutine methods (func Name() coroutine) with start and
yield / wait / frames / fixed / await; signals with Emit/Connect and
Connection handles; ScriptRef<T> and node.GetScript<T>() for fields,
plain methods and signals of other scripts. Diagnostics AES2050-AES2059.

Bytecode v3: ScriptRefs, XFields, XMethods and Connects tables resolved by
name in vm.Link, per-script signals, coroutine flag on functions; new
opcodes START/YIELD/EMIT/CONNECT/DISCONNECT/CONNECTED/COSTOP/CORUN/
SELFREF/GETSCRIPT/GETXF/SETXF/CALLR, checked by verify.

Runtime: script/coroutine scheduler (frames/fixed counters, time heap,
await lists; only due coroutines are visited) and script/signal registry
(snapshot per Emit, deferred removal, no allocations in steady state).
Hot reload cancels coroutines of changed scripts, rebinds connections by
name and signature, then runs Reloaded.

Tooling: script/build removed, aego check/test/dump run on the workspace
(test builds include _test.aes); langsvc completion, hover, signatures and
semantic tokens for the new syntax; editor highlighting, snippets with
placeholders, coroutine/signal profiler graphs and stats.

Docs (script.md sections 16-19, editor.md, ARCHITECTURE), test fixtures and
golden files, platformer enemies wired to the player, scriptreload demo,
and run.Config.FrameTime for deterministic headless fixed steps.

Modules (6.5): engine/module registry and ComponentBuilder are the one
registration path for engine subsystems and native code; render2d,
render3d and script/engine are modules over scene/frame, the script
component moved to scene/scriptcomp, and scene/components and
scene/systems are gone. Editor inspector, templates, picking shapes and
extension hooks are driven by descriptors and editor/builtins; sdk gains
Component/EditorHook; depcheck checks the module graph and the
aego_runtime graph.

Also: stop tracking built tool binaries; gl33 activates the scratch unit
before binding for texture updates.

Verification: typed component method adapters (Method0..2, Proc0..2)
with direct field access and no allocations; app.Build as the single
construction path for run and the editor; modules attach per scene
document; depcheck aego_editor pass; diagnostic codes in one place and
every code covered by a test; indexed coroutine heaps with eager
cancel; dead component references and nil intrinsics fault with
AES6004, host faults keep their code; GC roots of struct fields checked
in verify and vm.Link; 255-field limit; AES4010 for script packages
shadowing host modules; architecture registry D111-D147.

Golden: the harness renders on the thread that owns the GL context,
ReadPixels returns rows in memory order (render targets are top-down per
D17), and the first rendering references are committed.
2026-10-12 14:22:08 +03:00
sanspie 08f0b15e1e docs: update architecture spec and script reference 2026-10-08 13:31:21 +03:00
sanspie 17cb5e4291 Stage 2-5: assets, scene/ECS, editor, 3D, and Aego Script
Stage 2: render pipeline and UI toolkit over gpu/platform.
Stage 3: schema registry, asset pipeline + runtime loader, scene graph and ECS.
Stage 4: editor — docking, inspector, scene/game views, play mode, asset browser, console, profiler; 4.5 adds the 3D foundation (mixed 2D/3D spaces, meshes, depth).
Stage 5: Aego Script — compiler (lexer/parser/types/resolver/checker/asm/compiler), bytecode with verifier and register VM, instance runtime, stdlib, native bindings, and editor/CLI integration (aego run/edit/check/test/dump).
2026-10-08 13:17:28 +03:00
sanspie eb9ac1e589 Initial commit 2026-09-26 16:32:40 +03:00
sanspie dae4bc19e1 Stage 1: engine core, platform, gpu, vfs
Foundation for the aego engine.

core/    errs, log, handle arena, mathx, clock, stats
platform SDL3 backend plus a headless one, input state, events
gpu      device abstraction with gl33 and null backends
vfs      scheme-based virtual filesystem with overlay mounts
engine   builder, module registry, phase-ordered system loop, jobs
demos    basic, resize, input, headless; sandbox driver

OpenGL bindings are generated into gpu/gl33/gl by tools/gen-gl.sh,
so the module has no external dependencies.
2026-09-18 20:51:54 +03:00
542 changed files with 1051 additions and 33293 deletions
-4
View File
@@ -37,10 +37,6 @@ jobs:
- run: go vet ./platform/sdl
- run: make lint
- run: make depcheck
- name: packaging tools
run: sudo apt-get install -y nsis squashfs-tools
- name: installer dry run
run: make nsis-check
build:
strategy:
+26 -142
View File
File diff suppressed because it is too large Load Diff
+6 -55
View File
File diff suppressed because it is too large Load Diff
-8
View File
@@ -1,8 +0,0 @@
# Aego
A 2D and 3D game engine with an editor, a scripting language and a web export, written in Go.
- Documentation and the playground: <https://aego.akarpov.ru>
- Installing and building from source: [engine/res/docs/install.md](engine/res/docs/install.md)
- Contributing, check levels and rules for engine code: [docs/contributing.md](docs/contributing.md)
- Architecture and decisions: [ARCHITECTURE.md](ARCHITECTURE.md)
+3 -49
View File
@@ -2,7 +2,6 @@ package agent
import (
"errors"
"maps"
"os"
"path"
"slices"
@@ -66,8 +65,9 @@ func sceneCommands() []*Command {
{Name: "scene.rename", Doc: "rename a node", Write: true, Args: []Arg{argFile, argNode, {Name: "name", Type: "string", Doc: "new name", Required: true, Pos: true}}, Run: func(s *Session, a Args) (*Output, error) {
return s.sceneTx(a, "Rename", mutate.Rename(a.Str("node"), a.Str("name")))
}},
{Name: "scene.add-comp", Doc: "add a component with its defaults and the components it requires", Write: true, Args: []Arg{argFile, argNode, {Name: "comp", Type: "string", Doc: "component name, full or a unique suffix (Body2D)", Required: true, Pos: true},
{Name: "no-required", Type: "boolean", Doc: "add only this component; missing requirements stay as problems"}}, Run: (*Session).sceneAddComp},
{Name: "scene.add-comp", Doc: "add a component with its defaults", Write: true, Args: []Arg{argFile, argNode, {Name: "comp", Type: "string", Doc: "component name, full or a unique suffix (Body2D)", Required: true, Pos: true}}, Run: func(s *Session, a Args) (*Output, error) {
return s.sceneTx(a, "Add Component", mutate.AddComp(a.Str("node"), a.Str("comp")))
}},
{Name: "scene.remove-comp", Doc: "remove a component", Write: true, Args: []Arg{argFile, argNode, {Name: "comp", Type: "string", Doc: "component name", Required: true, Pos: true}}, Run: func(s *Session, a Args) (*Output, error) {
return s.sceneTx(a, "Remove Component", mutate.RemoveComp(a.Str("node"), a.Str("comp")))
}},
@@ -93,49 +93,6 @@ func (s *Session) scene(a Args) (string, []byte, *mutate.Document, error) {
return rel, src, doc, err
}
// sceneAddComp adds a component and, unless no-required is set, the components it requires first, in one transaction (D144).
func (s *Session) sceneAddComp(a Args) (*Output, error) {
rel, src, doc, err := s.scene(a)
if err != nil {
return nil, err
}
node, comp := a.Str("node"), a.Str("comp")
ops := []mutate.Op{mutate.AddComp(node, comp)}
var notes []string
if !a.Bool("no-required") {
td, err := doc.Component(comp)
if err != nil {
return nil, err
}
h, err := doc.Node(node)
if err != nil {
return nil, err
}
req, err := mutate.Required(doc.Scene, td, h)
if err != nil {
return nil, err
}
ops = ops[:0]
for _, r := range req {
ops = append(ops, mutate.AddComp(node, r.FullName))
notes = append(notes, "added "+label(r)+" (required by "+label(td)+")")
}
ops = append(ops, mutate.AddComp(node, td.FullName))
}
out, err := s.applyDoc(a, rel, src, doc, mutate.Tx{Label: "Add Component", Ops: ops})
if out != nil {
out.Notes = append(notes, out.Notes...)
}
return out, err
}
func label(td *schema.TypeDescriptor) string {
if td.DisplayName != "" {
return td.DisplayName
}
return td.FullName
}
func (s *Session) sceneTx(a Args, label string, ops ...mutate.Op) (*Output, error) {
rel, src, doc, err := s.scene(a)
if err != nil {
@@ -288,9 +245,6 @@ func (s *Session) sceneRun(a Args) (*Output, error) {
}
var b strings.Builder
b.WriteString("frames: " + strconv.Itoa(rep.Frames) + "\nhash: " + rep.Hash + "\n")
for _, k := range slices.Sorted(maps.Keys(rep.Counters)) {
b.WriteString(k + ": " + strconv.FormatUint(rep.Counters[k], 10) + "\n")
}
for _, l := range rep.Log {
b.WriteString(l + "\n")
}
-56
View File
@@ -1,7 +1,6 @@
package agent
import (
"aego/script/abi"
"errors"
"strings"
@@ -35,14 +34,6 @@ type TypeView struct {
Requires []string `json:"requires,omitempty"`
Doc string `json:"doc,omitempty"`
Fields []FieldView `json:"fields,omitempty"`
Methods []CallView `json:"methods,omitempty"`
Signals []CallView `json:"signals,omitempty"`
}
type CallView struct {
Name string `json:"name"`
Sig string `json:"sig"`
Doc string `json:"doc,omitempty"`
}
func schemaCommands() []*Command {
@@ -99,7 +90,6 @@ func (s *Session) lookupType(name string) (*schema.TypeDescriptor, error) {
found = append(found, td)
}
}
found = schema.PreferProject(found)
switch len(found) {
case 1:
return found[0], nil
@@ -172,51 +162,5 @@ func (s *Session) schemaShow(a Args) (*Output, error) {
}
b.WriteString("\n")
}
if s.kit != nil {
for _, cm := range s.kit.methods(td.FullName) {
v.Methods = append(v.Methods, CallView{Name: cm.Name, Sig: methodSig(cm.Sig, cm.Params), Doc: summary(cm.Doc)})
}
}
for _, sg := range td.Signals {
params := make([]string, len(sg.Params))
for i, p := range sg.Params {
params[i] = p
if i < len(sg.Names) && sg.Names[i] != "" {
params[i] = sg.Names[i] + " " + p
}
}
v.Signals = append(v.Signals, CallView{Name: sg.Name, Sig: "(" + strings.Join(params, ", ") + ")", Doc: summary(sg.Doc)})
}
for _, sec := range []struct {
title string
list []CallView
}{{"methods", v.Methods}, {"signals", v.Signals}} {
if len(sec.list) == 0 {
continue
}
b.WriteString(sec.title + ":\n")
for _, c := range sec.list {
b.WriteString(" " + c.Name + c.Sig)
if c.Doc != "" {
b.WriteString(" — " + c.Doc)
}
b.WriteString("\n")
}
}
return &Output{Value: v, Text: b.String()}, nil
}
func methodSig(sig abi.Signature, names []string) string {
params := make([]string, len(sig.Params))
for i, p := range sig.Params {
params[i] = p.String()
if i < len(names) && names[i] != "" {
params[i] = names[i] + " " + params[i]
}
}
out := "(" + strings.Join(params, ", ") + ")"
if sig.Result.Kind != abi.Void {
out += " " + sig.Result.String()
}
return out
}
-21
View File
@@ -28,7 +28,6 @@ import (
"aego/scene/sceneio"
"aego/schema"
"aego/script/bind"
"aego/script/bind/manifest"
"aego/script/reload"
"aego/script/runtime"
"aego/script/workspace"
@@ -49,7 +48,6 @@ type RunRequest struct {
type RunReport struct {
Frames int `json:"frames"`
Hash string `json:"hash"`
Counters map[string]uint64 `json:"counters,omitempty"`
Log []string `json:"log,omitempty"`
Error string `json:"error,omitempty"`
}
@@ -81,25 +79,6 @@ type Kit struct {
ws *workspace.Workspace
binds *bind.Registry
opt Options
man *manifest.Manifest
}
// methods lists the script methods the bindings give a component type, built once per kit.
func (k *Kit) methods(comp string) []manifest.CompMethod {
if k.man == nil {
m, err := manifest.FromRegistry(k.binds, k.Types.Schema())
if err != nil {
return nil
}
k.man = m
}
var out []manifest.CompMethod
for _, cm := range k.man.CompMethods {
if cm.Comp == comp {
out = append(out, cm)
}
}
return out
}
func NewKit(p *project.Project, opt Options) (*Kit, error) {
+1 -6
View File
@@ -10,7 +10,6 @@ import (
"aego/modules/dataasset"
"aego/modules/debug"
"aego/modules/display"
"aego/modules/gameplay"
inputmod "aego/modules/input"
"aego/modules/physics"
"aego/modules/render2d"
@@ -77,10 +76,6 @@ func cameraEntry() assembly.Entry {
return assembly.Entry{ID: camera.ID, Requires: []string{scripting.ID}, New: func(assembly.Options) module.Module { return camera.New() }}
}
func gameplayEntry() assembly.Entry {
return assembly.Entry{ID: gameplay.ID, Requires: []string{physics.ID, scripting.ID}, New: func(assembly.Options) module.Module { return gameplay.New() }}
}
func dataEntry() assembly.Entry {
return assembly.Entry{ID: dataasset.ID, New: func(assembly.Options) module.Module { return dataasset.New() }}
}
@@ -95,5 +90,5 @@ func savegameEntry() assembly.Entry {
func Default() assembly.Catalog {
return assembly.Catalog{core(), dataEntry(), inputEntry(), render2dEntry(), render3dEntry(), scriptEntry(), physicsEntry(), debugEntry(),
displayEntry(), audioEntry(), accessibilityEntry(), settingsEntry(), tweenEntry(), cameraEntry(), gameplayEntry(), spawnEntry(), savegameEntry()}
displayEntry(), audioEntry(), accessibilityEntry(), settingsEntry(), tweenEntry(), cameraEntry(), spawnEntry(), savegameEntry()}
}
-6
View File
@@ -2,14 +2,10 @@ package editors
import (
"aego/app/assembly"
"aego/modules/audio"
audioeditor "aego/modules/audio/editor"
"aego/modules/camera"
cameraeditor "aego/modules/camera/editor"
"aego/modules/debug"
debugeditor "aego/modules/debug/editor"
"aego/modules/gameplay"
gameplayeditor "aego/modules/gameplay/editor"
inputmod "aego/modules/input"
inputeditor "aego/modules/input/editor"
"aego/modules/physics"
@@ -27,7 +23,5 @@ func With(c assembly.Catalog) assembly.Catalog {
With(render3d.ID, render3deditor.Hook).
With(physics.ID, physicseditor.Hook).
With(camera.ID, cameraeditor.Hook).
With(gameplay.ID, gameplayeditor.Hook).
With(audio.ID, audioeditor.Hook).
With(debug.ID, debugeditor.Hook)
}
-4
View File
@@ -4,10 +4,8 @@ import (
"aego/app/assembly"
"aego/engine/base"
"aego/engine/module"
"aego/modules/audio"
"aego/modules/camera"
"aego/modules/debug"
"aego/modules/gameplay"
inputmod "aego/modules/input"
"aego/modules/physics"
"aego/modules/render2d"
@@ -32,11 +30,9 @@ func Catalog() assembly.Catalog {
{ID: debug.ID, New: func(assembly.Options) module.Module { return debug.New() }},
{ID: tween.ID, Requires: []string{scripting.ID}, New: func(assembly.Options) module.Module { return tween.New() }},
{ID: camera.ID, Requires: []string{scripting.ID}, New: func(assembly.Options) module.Module { return camera.New() }},
{ID: gameplay.ID, Requires: []string{physics.ID, scripting.ID}, New: func(assembly.Options) module.Module { return gameplay.New() }},
{ID: render3d.ID, New: func(assembly.Options) module.Module { return render3d.New() }},
{ID: spawn.ID, New: func(assembly.Options) module.Module { return spawn.New() }},
{ID: savegame.ID, Requires: []string{scripting.ID, spawn.ID}, New: func(assembly.Options) module.Module { return savegame.New() }},
{ID: audio.ID, New: func(assembly.Options) module.Module { return audio.New() }},
}
}
-2
View File
@@ -9,7 +9,6 @@ import (
"sort"
"strings"
"aego/app/proc"
"aego/core/errs"
"aego/core/log"
"aego/project"
@@ -170,7 +169,6 @@ func Build(p *project.Project, t Target, mode Mode, engineVersion string, lg *lo
cmd := exec.Command("go", args...)
cmd.Dir = p.Root
cmd.Env = append(os.Environ(), "GOOS="+t.OS, "GOARCH="+t.Arch)
proc.Hide(cmd)
output, err := cmd.CombinedOutput()
if err != nil {
os.RemoveAll(outDir)
-3
View File
@@ -8,7 +8,6 @@ import (
"path/filepath"
"strings"
"aego/app/proc"
"aego/core/errs"
"aego/core/log"
"aego/project"
@@ -33,14 +32,12 @@ func RunIcons(p *project.Project, engineVersion string, out io.Writer) (int, err
defer os.Remove(bin)
build := exec.Command("go", "build", "-tags", strings.Join(t.Tags, ","), "-o", bin, "./.aego/generated/icons")
build.Dir = p.Root
proc.Hide(build)
if output, err := build.CombinedOutput(); err != nil {
return 2, errs.New(errs.BuildFailed, "native icons helper does not compile",
log.F("output", strings.TrimSpace(string(output))))
}
run := exec.Command(bin)
run.Dir, run.Stdout, run.Stderr = p.Root, out, out
proc.Hide(run)
err = run.Run()
var ee *exec.ExitError
if errors.As(err, &ee) {
-3
View File
@@ -6,7 +6,6 @@ import (
"path/filepath"
"strings"
"aego/app/proc"
"aego/core/errs"
"aego/core/log"
"aego/project"
@@ -53,7 +52,6 @@ func BuildManifest(p *project.Project, engineVersion string, lg *log.Logger) (st
defer os.Remove(bin)
build := exec.Command("go", "build", "-tags", strings.Join(t.Tags, ","), "-o", bin, "./.aego/generated/manifest")
build.Dir = p.Root
proc.Hide(build)
if output, err := build.CombinedOutput(); err != nil {
return "", errs.New(errs.BuildFailed, "native bindings helper does not compile",
log.F("output", strings.TrimSpace(string(output))))
@@ -61,7 +59,6 @@ func BuildManifest(p *project.Project, engineVersion string, lg *log.Logger) (st
tmp := out + ".tmp"
run := exec.Command(bin, tmp)
run.Dir = p.Root
proc.Hide(run)
if output, err := run.CombinedOutput(); err != nil {
os.Remove(tmp)
return "", errs.New(errs.BuildFailed, "native module Register failed",
-33
View File
@@ -1,33 +0,0 @@
package brand
import (
"bytes"
"embed"
"image"
"image/png"
"strconv"
)
//go:embed png/aego-*.png
var files embed.FS
var windowSizes = []int{16, 24, 32, 48, 64, 128, 256}
func PNG(n int) ([]byte, bool) {
data, err := files.ReadFile("png/aego-" + strconv.Itoa(n) + ".png")
return data, err == nil
}
func Window() []image.Image {
out := make([]image.Image, 0, len(windowSizes))
for _, n := range windowSizes {
data, ok := PNG(n)
if !ok {
continue
}
if img, err := png.Decode(bytes.NewReader(data)); err == nil {
out = append(out, img)
}
}
return out
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 563 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 878 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

+3 -5
View File
@@ -41,7 +41,6 @@ type Config struct {
Editor bool
FailOnScriptErrors bool
Catalog assembly.Catalog
Platform platform.Platform
Profile string
NoProfile bool
}
@@ -99,9 +98,6 @@ func (h *Host) build(cfg Config, mods []module.Module) error {
ecfg.LoopScope = cfg.LoopScope
}
builder := engine.NewBuilder(ecfg).WithLogger(lg).Use(rmod)
if cfg.Platform != nil {
builder.WithPlatform(cfg.Platform)
}
if h.Types, err = assembly.NewTypes(); err != nil {
return err
}
@@ -191,7 +187,6 @@ func (h *Host) build(cfg Config, mods []module.Module) error {
}
h.Settings.SetNames(h.Sections.Names)
h.Engine.SetService(settings.ServiceID, h.Settings)
h.Engine.OnClose(h.Modules.Shutdown)
if err = h.Modules.Init(&module.InitContext{Engine: h.Engine, Assets: h.Loader, Log: lg, Editor: cfg.Editor, Sections: h.Sections, Problems: h.Problems, Settings: h.Settings}); err != nil {
return err
}
@@ -232,6 +227,9 @@ func (h *Host) Close() {
return
}
h.closed = true
if h.Modules != nil {
h.Modules.Shutdown()
}
if h.Engine != nil {
h.Engine.Close()
}
-70
View File
@@ -1,70 +0,0 @@
package docstool
import (
"fmt"
"io"
"strings"
"aego/app/playground"
"aego/engine/docs"
"aego/engine/module"
)
// CheckExamples loads the example scene of every module code, validates it and runs it headless; the code must be among the problems it reports (D146).
func CheckExamples(reg *docs.Registry, w io.Writer) int {
pg, err := playground.New()
if err != nil {
fmt.Fprintln(w, "examples: cannot start the docs runtime:", err)
return 1
}
pg.SetGame(playground.GameConfig{Headless: true, FrameTime: 1.0 / 60})
defer func() {
if g := pg.Game(); g != nil {
g.Close()
}
}()
bad := 0
for _, e := range reg.All() {
if e.Kind != docs.Code || e.Diag == nil || e.Diag.Example == "" {
continue
}
found, why := false, ""
code := ""
if e.Diag.ExampleScript != "" {
c, err := pg.TryCode(e.Diag.ExampleScript)
if err != nil {
fmt.Fprintf(w, "%s: error: example script does not compile: %v\n", e.ID, err)
bad++
continue
}
code = c
}
if res := pg.PlayScene("example-"+e.ID, []byte(e.Diag.Example), code, nil); !res.Started {
why = "does not load: " + res.Error
} else {
for _, d := range module.ValidateScene(pg.Game().Scene(), nil) {
found = found || d.Code == e.ID
}
if err := pg.Game().Step(tryFrames); err != nil {
why = "stops the engine: " + err.Error()
}
}
poll := pg.Poll()
for _, f := range poll.Faults {
found = found || f.Code == e.ID
}
for _, l := range poll.Log {
found = found || strings.Contains(l.Text, e.ID)
}
switch {
case why != "":
fmt.Fprintf(w, "%s: error: example %s %s\n", e.ID, e.Diag.ExampleFile, why)
bad++
case !found:
fmt.Fprintf(w, "%s: error: example of %s does not produce %s\n", e.ID, e.ID, e.ID)
bad++
}
}
pg.Stop()
return bad
}
-3
View File
@@ -76,9 +76,6 @@ func (e *Env) Check(strict bool) int {
if CheckTry(reg, e.Stdout) > 0 && code == 0 {
code = 1
}
if CheckExamples(reg, e.Stdout) > 0 && code == 0 {
code = 1
}
return code
}
-13
View File
@@ -19,7 +19,6 @@ type request struct {
Component string `json:"component"`
Field string `json:"field"`
Value float64 `json:"value"`
Node string `json:"node"`
}
type Reply struct {
@@ -28,7 +27,6 @@ type Reply struct {
Error string `json:"error,omitempty"`
State *docsession.State `json:"state,omitempty"`
Log []Line `json:"log,omitempty"`
Pose []float64 `json:"pose,omitempty"`
}
func (a *App) Call(method string, payload []byte) Reply {
@@ -66,17 +64,6 @@ func (a *App) Call(method string, payload []byte) Reply {
case "state":
st := s.State()
return Reply{State: &st}
case "pose":
sc := s.Controller().Scene()
if sc == nil {
return Reply{Error: "the scene is not running"}
}
h := sc.Find(sc.Root(), req.Node)
if h.IsZero() || !sc.IsAlive(h) {
return Reply{Error: "no node " + strconv.Quote(req.Node)}
}
p := sc.WorldPosition(h)
return Reply{Pose: []float64{float64(p.X), float64(p.Y)}}
case "log":
out := Reply{Log: a.lines}
a.lines = nil
-27
View File
@@ -1,27 +0,0 @@
package edit
import (
"os"
"os/exec"
"runtime"
"strings"
"aego/engine"
"aego/platform"
)
func startBoot(verb string) {
wd, _ := os.Getwd()
exe, _ := os.Executable()
gobin, err := exec.LookPath("go")
if err != nil {
gobin = "not on PATH"
}
_ = platform.StartBoot(platform.DefaultBootPath(),
"aego "+engine.Version+" "+runtime.GOOS+"/"+runtime.GOARCH+" "+verb,
"exe "+exe,
"args "+strings.Join(os.Args[1:], " "),
"cwd "+wd,
"go "+gobin,
)
}
+48 -12
View File
File diff suppressed because it is too large Load Diff
-163
View File
File diff suppressed because it is too large Load Diff
-49
View File
@@ -1,19 +1,14 @@
package edit
import (
"net"
"net/http"
"os/exec"
"path/filepath"
"runtime"
"sync"
"aego/app/bootstrap"
"aego/app/export"
"aego/app/projects"
"aego/asset/pipeline"
"aego/editor/core/services"
"aego/editor/core/sysopen"
"aego/engine/problems"
"aego/project"
)
@@ -92,47 +87,3 @@ func (x *exporter) Reveal(path string) error {
}
return cmd.Start()
}
var webServers sync.Map
func serveWeb(dir string) (string, error) {
if u, ok := webServers.Load(dir); ok {
return u.(string), nil
}
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return "", err
}
files := http.FileServer(http.Dir(dir))
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cross-Origin-Opener-Policy", "same-origin")
w.Header().Set("Cross-Origin-Embedder-Policy", "require-corp")
w.Header().Set("Cache-Control", "no-store")
files.ServeHTTP(w, r)
})
go http.Serve(ln, h)
u := "http://" + ln.Addr().String() + "/"
webServers.Store(dir, u)
return u, nil
}
func (x *exporter) Run(out, target string) error {
path, err := export.Launchable(x.p, target, out)
if err != nil {
return err
}
t, _ := export.ParseTarget(target)
switch {
case t.Web():
u, err := serveWeb(path)
if err != nil {
return err
}
return sysopen.Open(u)
case t.OS == "darwin":
return exec.Command("open", path).Start()
}
cmd := exec.Command(path)
cmd.Dir = filepath.Dir(path)
return cmd.Start()
}
+5 -82
View File
File diff suppressed because it is too large Load Diff
-50
View File
@@ -1,50 +0,0 @@
package edit
import (
"path/filepath"
"testing"
"aego/app/projects"
"aego/editor/shell/launcher"
"aego/project"
)
func TestLauncherCreatesAndOpens(t *testing.T) {
recent := filepath.Join(t.TempDir(), "recent")
ls, err := OpenLauncher(Config{Headless: true, Recent: recent})
if err != nil {
t.Fatal(err)
}
ls.UI.SetTab(launcher.TabNew)
dir := filepath.Join(t.TempDir(), "Aego Projects", "game")
if _, err := ls.UI.Create(dir, projects.Template2D, "", ""); err != nil {
ls.Close()
t.Fatal(err)
}
for i := 0; i < 3 && ls.Step(); i++ {
}
ls.Close()
root, ok := ls.UI.Result()
if !ok {
t.Fatal("the launcher has no result")
}
p, err := project.Load(root)
if err != nil {
t.Fatal(err)
}
s, err := Open(p, Config{Headless: true, NoWatch: true, Recent: recent})
if err != nil {
t.Fatal(err)
}
defer s.Close()
for i := 0; i < 3; i++ {
s.Step()
}
if s.App.Context().Docs.Active() == nil {
t.Fatal("no scene is open after creating the project")
}
list, _ := launcher.LoadRecent(recent)
if len(list) == 0 || list[0].Path != root {
t.Fatalf("recent list %+v", list)
}
}
-78
View File
@@ -1,78 +0,0 @@
package edit
import (
"io"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
"aego/core/log"
)
const keepLogs = 10
type logFile struct {
f *os.File
}
func LogsDir() string {
dir, err := os.UserCacheDir()
if err != nil {
return ""
}
return filepath.Join(dir, "aego", "logs")
}
func attachLogs(cfg *Config) *logFile {
if cfg.Logger == nil {
cfg.Logger = log.New(log.NewTextSink(os.Stderr))
if cfg.Debug {
cfg.Logger.SetLevel(log.Debug)
}
}
dir := LogsDir()
if dir == "" || os.MkdirAll(dir, 0o755) != nil {
return &logFile{}
}
name := "editor-" + time.Now().Format("20060102-150405") + "-" + strconv.Itoa(os.Getpid()) + ".log"
f, err := os.Create(filepath.Join(dir, name))
if err != nil {
return &logFile{}
}
cfg.Logger.AddSink(log.NewTextSink(f))
pruneLogs(dir)
return &logFile{f: f}
}
func (l *logFile) writer(w io.Writer) io.Writer {
if l.f == nil {
return w
}
return io.MultiWriter(w, l.f)
}
func (l *logFile) close() {
if l.f != nil {
l.f.Close()
}
}
func pruneLogs(dir string) {
entries, err := os.ReadDir(dir)
if err != nil {
return
}
var names []string
for _, e := range entries {
if !e.IsDir() && strings.HasSuffix(e.Name(), ".log") {
names = append(names, e.Name())
}
}
sort.Strings(names)
for i := 0; i+keepLogs < len(names); i++ {
os.Remove(filepath.Join(dir, names[i]))
}
}
+1 -1
View File
@@ -179,7 +179,7 @@ func TestProfilerSectionsFollowPlay(t *testing.T) {
s.Step()
}
drawPanel(s, prof)
expectSections(t, prof.Sections(), "Frame", "Memory", "Game", "Scripts", "Physics", "Audio", "Camera", "Gameplay")
expectSections(t, prof.Sections(), "Frame", "Memory", "Game", "Scripts", "Physics")
if _, err := ctx.Docs.Open("scenes/other.aescene"); err != nil {
t.Fatal(err)
}
-77
View File
@@ -1,77 +0,0 @@
package edit
import (
"os"
"os/exec"
"strings"
"aego/app/proc"
"aego/editor/core/session"
)
const (
EnvEditorExe = "AEGO_EDITOR_EXE"
EnvEditorVerb = "AEGO_EDITOR_VERB"
)
func markEntry(verb string) {
if os.Getenv(EnvEditorExe) != "" {
return
}
if img := os.Getenv("APPIMAGE"); img != "" {
os.Setenv(EnvEditorExe, img)
os.Setenv(EnvEditorVerb, "")
return
}
exe, err := os.Executable()
if err != nil {
return
}
os.Setenv(EnvEditorExe, exe)
os.Setenv(EnvEditorVerb, verb)
}
func relaunchArgs(verb, target, tab string) []string {
if verb != "" {
args := []string{verb}
if target != "" {
args = append(args, target)
}
if tab != "" {
args = append(args, "--launcher", tab)
}
return args
}
var args []string
if tab != "" {
args = append(args, "-launcher", tab)
}
if target != "" {
args = append(args, target)
}
return args
}
func withoutEnv(env []string, key string) []string {
out := env[:0:0]
for _, kv := range env {
if !strings.HasPrefix(kv, key+"=") {
out = append(out, kv)
}
}
return out
}
func Relaunch(target, tab string) error {
exe := os.Getenv(EnvEditorExe)
if exe == "" {
var err error
if exe, err = os.Executable(); err != nil {
return err
}
}
cmd := exec.Command(exe, relaunchArgs(os.Getenv(EnvEditorVerb), target, tab)...)
cmd.Env = withoutEnv(os.Environ(), session.EnvSupervised)
proc.Hide(cmd)
return cmd.Start()
}
-77
View File
@@ -1,77 +0,0 @@
package edit
import (
"os"
"path/filepath"
"slices"
"testing"
"aego/project"
)
func TestRelaunchArgs(t *testing.T) {
cases := []struct {
verb, target, tab string
want []string
}{
{"", "/p", "", []string{"/p"}},
{"", "", "recent", []string{"-launcher", "recent"}},
{"edit", "/p", "", []string{"edit", "/p"}},
{"edit", "", "new", []string{"edit", "--launcher", "new"}},
}
for _, c := range cases {
if got := relaunchArgs(c.verb, c.target, c.tab); !slices.Equal(got, c.want) {
t.Errorf("%q %q %q: %v, want %v", c.verb, c.target, c.tab, got, c.want)
}
}
env := withoutEnv([]string{"A=1", "AEGO_SUPERVISED=1", "B=2"}, "AEGO_SUPERVISED")
if !slices.Equal(env, []string{"A=1", "B=2"}) {
t.Fatal(env)
}
}
func TestResolveArg(t *testing.T) {
root := t.TempDir()
os.WriteFile(filepath.Join(root, project.FileName), []byte("project version=1 {\n}\n"), 0o644)
os.MkdirAll(filepath.Join(root, "scenes"), 0o755)
scene := filepath.Join(root, "scenes", "main.aescene")
os.WriteFile(scene, []byte("scene version=1 {\n}\n"), 0o644)
if dir, file := resolveArg(filepath.Join(root, project.FileName)); dir != root || file != "" {
t.Fatalf("project.aego: %q %q", dir, file)
}
dir, file := resolveArg(scene)
if dir != filepath.Join(root, "scenes") || file != scene {
t.Fatalf("scene: %q %q", dir, file)
}
if rel, ok := relInside(root, file); !ok || rel != "scenes/main.aescene" {
t.Fatalf("rel %q %v", rel, ok)
}
}
func TestNewer(t *testing.T) {
cases := []struct {
latest, current string
want bool
}{
{"v0.9.0", "0.9.0-beta.1", true},
{"v0.9.0-beta.1", "0.9.0-beta.1", false},
{"v0.9.0-beta.2", "0.9.0-beta.1", true},
{"v0.8.5", "0.9.0-beta.1", false},
{"v1.0.0", "0.9.0", true},
{"nightly", "0.9.0", false},
{"", "0.9.0", false},
}
for _, c := range cases {
if got := newer(c.latest, c.current); got != c.want {
t.Errorf("newer(%q, %q) = %v", c.latest, c.current, got)
}
}
file := filepath.Join(t.TempDir(), "update")
st := updateState{Enabled: false, Latest: "v1.2.3"}
if err := saveUpdate(file, st); err != nil {
t.Fatal(err)
}
if back := loadUpdate(file); back.Enabled || back.Latest != "v1.2.3" {
t.Fatalf("round trip %+v", back)
}
}
+1 -67
View File
@@ -7,10 +7,8 @@ import (
"os"
"path/filepath"
"aego/core/ids"
"aego/editor/core/command"
"aego/editor/core/context"
"aego/editor/core/vectors"
"aego/editor/core/workspace"
"aego/editor/panels/gameview"
"aego/editor/shell"
@@ -58,60 +56,7 @@ func (s *Session) shots(dir string) error {
}
return selectNamed(ctx, "Player")
}},
{name: "hitbox-gizmo", frames: 60, setup: func(s *Session) error {
sc := ctx.Scene()
var span []ids.NodeID
for _, n := range []string{"EnemyLeft", "PatrolA1", "PatrolB1"} {
span = append(span, sc.ID(sc.Find(sc.Root(), n)))
}
ctx.Select(span...)
if err := ctx.Commands.Run(command.SceneFrame); err != nil {
return err
}
for i := 0; i < 30; i++ {
s.Step()
}
return selectNamed(ctx, "EnemyLeft")
}},
{name: "camera-follow", frames: 60, setup: func(s *Session) error {
sc := ctx.Scene()
ctx.Select(sc.ID(sc.Find(sc.Root(), "Camera")), sc.ID(sc.Find(sc.Root(), "Player")))
if err := ctx.Commands.Run(command.SceneFrame); err != nil {
return err
}
for i := 0; i < 30; i++ {
s.Step()
}
return selectNamed(ctx, "Camera")
}},
{name: "camera-zone", frames: 60, setup: func(s *Session) error {
sc := ctx.Scene()
ctx.Select(sc.ID(sc.Find(sc.Root(), "ArenaZone")), sc.ID(sc.Find(sc.Root(), "PatrolA2")), sc.ID(sc.Find(sc.Root(), "PatrolB2")), sc.ID(sc.Find(sc.Root(), "GroundRight")))
if err := ctx.Commands.Run(command.SceneFrame); err != nil {
return err
}
for i := 0; i < 30; i++ {
s.Step()
}
return selectNamed(ctx, "ArenaZone")
}},
{name: "clip-inspector", frames: 60, setup: func(s *Session) error {
id, ok := s.Pipeline.Lookup("assets/audio/music.ogg")
if !ok {
return fmt.Errorf("no assets/audio/music.ogg")
}
s.App.Data().Inspect(id)
return nil
}},
{name: "add-required", frames: 30, setup: func(s *Session) error {
if err := selectNamed(ctx, "PatrolA1"); err != nil {
return err
}
s.App.Inspector().OpenAdd("follow")
return nil
}},
{name: "tags", frames: 60, setup: func(s *Session) error {
ctx.UI.CloseAllPopups()
return selectNamed(ctx, "Player")
}},
{name: "prefab", frames: 60, setup: func(s *Session) error {
@@ -138,23 +83,12 @@ func (s *Session) shots(dir string) error {
scene()
return ctx.Commands.Run(command.PlayStart)
}},
{name: "motion-play", frames: 30, setup: func(s *Session) error {
s.App.Workspace().Show(workspace.Inspector)
s.App.Inspector().ScrollTo("gameplay.Motion2D")
return selectNamed(ctx, "Player")
}},
{name: "mixer", frames: 120, setup: func(s *Session) error {
s.App.Workspace().Switch(workspace.Audio)
s.App.Workspace().Show(workspace.Mixer)
return nil
}},
{name: "physics-debug", frames: 30, setup: func(s *Session) error {
s.App.Workspace().Switch(workspace.Design)
ctx.Overlay.Set("physics.debug.contacts", true)
return ctx.Commands.Run(gameview.CommandOverlays)
}},
{name: "demo", frames: 60, inset: 2, bottom: 28, setup: func(s *Session) error {
for _, f := range []string{gameview.CommandOverlays, vectors.CommandToggle} {
for _, f := range []string{"physics.debug", "physics.debug.shapes", "physics.debug.contacts", "physics.debug.joints"} {
ctx.Overlay.Set(f, false)
}
return ctx.Commands.Run(shell.CommandFullscreenPlay)
-17
View File
@@ -1,17 +0,0 @@
package edit
import (
"bytes"
"aego/app/run"
)
func (s *Session) runTests(done func(out string, failed bool)) error {
p, mods := s.Project, s.mods
go func() {
var buf bytes.Buffer
code := run.Tool(p, []string{"test", "-v"}, &buf, &buf, mods...)
s.Engine.Jobs().OnMain(func() { done(buf.String(), code != 0) })
}()
return nil
}
-194
View File
File diff suppressed because it is too large Load Diff
+1 -3
View File
@@ -12,7 +12,6 @@ import (
"aego/app/export/icon"
"aego/app/export/winres"
"aego/app/proc"
"aego/core/errs"
"aego/core/log"
"aego/runtime/bundle"
@@ -122,7 +121,7 @@ func (j *job) resources(dir string) error {
var icons []winres.Icon
if j.icon != nil {
for _, n := range winres.IconSizes {
data, err := icon.PNG(icon.Sized(j.icon, n))
data, err := icon.PNG(icon.Resize(j.icon, n, n))
if err != nil {
return err
}
@@ -176,7 +175,6 @@ func (j *job) build() error {
args = append(args, pkg)
cmd := exec.Command("go", args...)
cmd.Dir = root
proc.Hide(cmd)
cgo := "CGO_ENABLED=1"
if j.t.Web() {
cgo = "CGO_ENABLED=0"
+2 -3
View File
@@ -189,9 +189,8 @@ func (j *job) license(lib bundle.Lib, src, name string) bundle.Lib {
}
func (j *job) libs() []bundle.Lib {
vorbis := j.license(bundle.Lib{Name: "oggvorbis", Link: "static"}, filepath.Join(j.opt.Engine, "audio", "codec", "vorbis", "internal", "oggvorbis", "LICENSE"), "oggvorbis.txt")
if j.t.Web() {
return []bundle.Lib{j.license(bundle.Lib{Name: "Go wasm_exec.js", Link: "script", File: webExec}, filepath.Join(j.goroot, "LICENSE"), "Go.txt"), vorbis}
return []bundle.Lib{j.license(bundle.Lib{Name: "Go wasm_exec.js", Link: "script", File: webExec}, filepath.Join(j.goroot, "LICENSE"), "Go.txt")}
}
return []bundle.Lib{j.license(bundle.Lib{Name: "SDL3", Link: "static"}, j.sdlLicense(), "SDL3.txt"), vorbis}
return []bundle.Lib{j.license(bundle.Lib{Name: "SDL3", Link: "static"}, j.sdlLicense(), "SDL3.txt")}
}
-18
View File
@@ -307,21 +307,3 @@ func (j *job) prepareOut() error {
}
return nil
}
func Launchable(p *project.Project, target, out string) (string, error) {
t, err := ParseTarget(target)
if err != nil {
return "", err
}
if !t.Web() && !t.Host() {
return "", errs.New(errs.EngineConfig, "the build is for "+t.String()+"; copy it to that system to run it")
}
l := layoutFor(t, out, p)
switch {
case t.Web():
return l.root, nil
case t.OS == "darwin":
return l.app, nil
}
return l.binary(), nil
}
File diff suppressed because it is too large Load Diff
+1 -25
View File
@@ -13,12 +13,6 @@ import (
"aego/core/errs"
)
type DocType struct {
Name string
Extensions []string
Role string
}
type Info struct {
Name string
Executable string
@@ -28,7 +22,6 @@ type Info struct {
MinOS string
Category string
Copyright string
Documents []DocType
}
var icnsTypes = []struct {
@@ -87,23 +80,6 @@ func Plist(i Info, iconFile string) []byte {
str("LSMinimumSystemVersion", i.MinOS)
str("LSApplicationCategoryType", i.Category)
str("NSHumanReadableCopyright", i.Copyright)
if len(i.Documents) > 0 {
b.WriteString("\t<key>CFBundleDocumentTypes</key>\n\t<array>\n")
for _, d := range i.Documents {
role := d.Role
if role == "" {
role = "Editor"
}
b.WriteString("\t\t<dict>\n\t\t\t<key>CFBundleTypeName</key>\n\t\t\t<string>" + esc(d.Name) + "</string>\n")
b.WriteString("\t\t\t<key>CFBundleTypeRole</key>\n\t\t\t<string>" + esc(role) + "</string>\n")
b.WriteString("\t\t\t<key>CFBundleTypeExtensions</key>\n\t\t\t<array>\n")
for _, e := range d.Extensions {
b.WriteString("\t\t\t\t<string>" + esc(e) + "</string>\n")
}
b.WriteString("\t\t\t</array>\n\t\t</dict>\n")
}
b.WriteString("\t</array>\n")
}
b.WriteString("\t<key>NSHighResolutionCapable</key>\n\t<true/>\n")
b.WriteString("</dict>\n</plist>\n")
return []byte(b.String())
@@ -116,7 +92,7 @@ func ICNS(src image.Image) ([]byte, error) {
data, ok := cache[t.size]
if !ok {
var err error
if data, err = icon.PNG(icon.Sized(src, t.size)); err != nil {
if data, err = icon.PNG(icon.Resize(src, t.size, t.size)); err != nil {
return nil, err
}
cache[t.size] = data
-9
View File
@@ -23,12 +23,3 @@ func TestPlistEscapesAndBundleID(t *testing.T) {
t.Fatal(p)
}
}
func TestPlistDocumentTypes(t *testing.T) {
out := string(Plist(Info{Name: "Aego", Executable: "aego", ID: "ru.akarpov.aego", Documents: []DocType{{Name: "Aego Scene", Extensions: []string{"aescene"}}}}, ""))
for _, want := range []string{"<key>CFBundleDocumentTypes</key>", "<string>Aego Scene</string>", "<string>Editor</string>", "<string>aescene</string>"} {
if !strings.Contains(out, want) {
t.Fatalf("plist has no %s:\n%s", want, out)
}
}
}
+1 -1
View File
@@ -29,7 +29,7 @@ func (j *job) pack() error {
func (j *job) packLinux() error {
if j.icon != nil {
data, err := icon.PNG(icon.Sized(j.icon, 256))
data, err := icon.PNG(icon.Resize(j.icon, 256, 256))
if err != nil {
return err
}
+4 -26
View File
@@ -3,9 +3,7 @@ package export
import (
"bytes"
"compress/gzip"
"crypto/sha256"
"embed"
"encoding/hex"
"encoding/json"
"html/template"
"io/fs"
@@ -28,7 +26,6 @@ var webFS embed.FS
const (
CodeWebAsset = "EXP010"
CodeWebNative = "EXP011"
CodeWebMP3 = "EXP012"
WebWasm = "game.wasm"
WebFiles = "files.json"
WebBudget = 8 << 20
@@ -71,11 +68,7 @@ func CheckWeb(s *Snapshot, native bool) []problems.Diagnostic {
out = append(out, diag(CodeWebNative, problems.Warning,
"the native Go module is compiled to WebAssembly: it must build without cgo and must not use files, processes or the network", "native/"))
}
_, ffmpegErr := exec.LookPath("ffmpeg")
s.idx.Each(func(e *asset.Entry) {
if ffmpegErr != nil && strings.EqualFold(filepath.Ext(e.Source), ".mp3") {
out = append(out, diag(CodeWebMP3, problems.Warning, e.Source+" is an mp3; it is converted to ogg on import, which needs ffmpeg in PATH", e.Source))
}
if e.Missing || exportSkip(e.Kind) || !webUnsupported(e.Kind) {
return
}
@@ -143,21 +136,6 @@ func (j *job) webTitle() string {
return AppName(j.p)
}
// webBuild keys the page's ?v= URLs on the data, the runtime and both scripts, so a new engine gets new URLs under immutable caching.
func (j *job) webBuild(scripts ...[]byte) (string, error) {
wasm, err := os.ReadFile(j.l.binary())
if err != nil {
return "", errs.Wrap(err, errs.VFSIO, "cannot read the web runtime")
}
h := sha256.New()
h.Write([]byte(j.rep.Hash))
h.Write(wasm)
for _, s := range scripts {
h.Write(s)
}
return hex.EncodeToString(h.Sum(nil))[:buildKeyLen], nil
}
func (j *job) packWeb() error {
src, err := wasmExecPath(j.goroot)
if err != nil {
@@ -178,7 +156,7 @@ func (j *job) packWeb() error {
return err
}
if j.icon != nil {
data, err := icon.PNG(icon.Sized(j.icon, webIconSize))
data, err := icon.PNG(icon.Resize(j.icon, webIconSize, webIconSize))
if err != nil {
return err
}
@@ -193,9 +171,9 @@ func (j *job) packWeb() error {
if err := writeFile(filepath.Join(j.l.data, WebFiles), list, WebFiles); err != nil {
return err
}
build, err := j.webBuild(exec, loader)
if err != nil {
return err
build := j.rep.Hash
if len(build) > buildKeyLen {
build = build[:buildKeyLen]
}
var page bytes.Buffer
if err := webIndexTmpl.Execute(&page, webPage{Title: j.webTitle(), Build: build, Icon: j.icon != nil}); err != nil {
+3 -8
View File
@@ -18,14 +18,10 @@ canvas { display: block; width: 100%; height: 100%; outline: none; touch-action:
#status { color: #9a9890; font-size: 13px; }
.loader.failed #bar { background: #f0776c; }
#error { max-width: min(640px, 90vw); max-height: 40vh; overflow: auto; margin: 0; padding: 10px 12px; white-space: pre-wrap; color: #f0776c; background: #1d1f23; border-radius: 6px; font: 12px/1.5 ui-monospace, Menlo, Consolas, monospace; }
#fullscreen { position: fixed; top: 10px; right: 10px; width: 36px; height: 36px; border: 0; border-radius: 6px; background: rgba(0, 0, 0, 0.45); color: #fff; padding: 0; cursor: pointer; opacity: 0.5; }
#fullscreen { position: fixed; top: 10px; right: 10px; width: 36px; height: 36px; border: 0; border-radius: 6px; background: rgba(0, 0, 0, 0.45); color: #fff; font-size: 18px; cursor: pointer; opacity: 0.5; }
#fullscreen:hover { opacity: 1; }
#mute { position: fixed; top: 10px; right: 52px; width: 36px; height: 36px; border: 0; border-radius: 6px; background: rgba(0, 0, 0, 0.45); color: #fff; padding: 0; cursor: pointer; opacity: 0.5; }
#mute:hover { opacity: 1; }
#fullscreen svg, #mute svg { display: block; width: 18px; height: 18px; margin: auto; }
#mute .off, #mute.muted .on { display: none; }
#mute.muted .off { display: inline; }
</style>
<link rel="preload" href="game.wasm?v={{.Build}}" as="fetch" type="application/wasm" crossorigin>
</head>
<body>
<canvas id="game" data-aego tabindex="0" aria-label="{{.Title}}"></canvas>
@@ -35,8 +31,7 @@ canvas { display: block; width: 100%; height: 100%; outline: none; touch-action:
<div id="status">Loading…</div>
<pre id="error" hidden></pre>
</div>
<button id="fullscreen" type="button" title="Fullscreen" aria-label="Fullscreen" hidden><svg viewBox="0 0 16 16" aria-hidden="true"><path fill="currentColor" d="M9.92 13.44L10.25 13.38L13.38 13.38L13.38 10.25L13.44 9.92L13.76 9.52L14.08 9.39L14.58 9.44L14.98 9.76L15.12 10.25L15.12 14.25L15.06 14.58L14.74 14.98L14.25 15.12L10.25 15.12L9.92 15.06L9.52 14.74L9.38 14.25L9.52 13.76ZM13.44 6.08L13.38 5.75L13.38 2.62L10.25 2.62L9.92 2.56L9.52 2.24L9.39 1.92L9.44 1.42L9.76 1.02L10.25 0.88L14.25 0.88L14.58 0.94L14.98 1.26L15.12 1.75L15.12 5.75L15.06 6.08L14.74 6.48L14.25 6.62L13.76 6.48ZM2.56 9.92L2.62 10.25L2.62 13.38L5.75 13.38L6.08 13.44L6.48 13.76L6.62 14.25L6.56 14.58L6.24 14.98L5.75 15.12L1.75 15.12L1.42 15.06L1.02 14.74L0.88 14.25L0.88 10.25L0.94 9.92L1.26 9.52L1.75 9.38L2.24 9.52ZM6.08 2.56L5.75 2.62L2.62 2.62L2.62 5.75L2.56 6.08L2.24 6.48L1.75 6.62L1.42 6.56L1.02 6.24L0.88 5.75L0.88 1.75L0.94 1.42L1.26 1.02L1.75 0.88L5.75 0.88L6.08 0.94L6.48 1.26L6.62 1.75L6.48 2.24Z"/></svg></button>
<button id="mute" type="button" title="Mute" aria-label="Mute" hidden><svg viewBox="0 0 16 16" aria-hidden="true"><path fill="currentColor" d="M1.81 6.04L4 6L6.74 3.47L7.11 3.41L7.36 3.55L7.5 3.9L7.5 12.1L7.36 12.45L7.11 12.59L6.74 12.53L4 10L2 10L1.65 9.85L1.5 9.5L1.5 6.5L1.58 6.22Z"/><path class="on" fill="currentColor" d="M12.53 5.89L11.97 4.96L11.01 3.9L10.95 3.62L10.99 3.33L11.15 3.08L11.39 2.91L11.82 2.86L12.21 3.05L12.72 3.54L13.53 4.59L13.91 5.29L14.17 5.95L14.35 6.63L14.5 7.96L14.46 8.75L14.34 9.45L14.14 10.13L13.58 11.34L13.18 11.92L12.67 12.52L12.09 13.04L11.67 13.15L11.39 13.09L11.15 12.92L10.99 12.67L10.95 12.38L11.09 11.97L11.61 11.46L12.27 10.59L12.73 9.61L12.88 9.08L12.97 8.54L12.97 7.46L12.73 6.39ZM10.05 7.09L9.82 6.68L9.38 6.19L9.26 5.77L9.31 5.48L9.48 5.24L9.72 5.08L10.01 5.02L10.29 5.08L10.54 5.24L11.01 5.76L11.27 6.16L11.66 7.18L11.75 8.04L11.58 9.12L11.23 9.91L10.54 10.76L10.29 10.92L10.01 10.98L9.72 10.92L9.48 10.76L9.31 10.52L9.26 10.23L9.38 9.81L9.95 9.12L10.2 8.47L10.24 7.76Z"/><path class="off" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" d="M10.25 5.75l4.5 4.5M14.75 5.75l-4.5 4.5"/></svg></button>
<button id="fullscreen" type="button" title="Fullscreen" aria-label="Fullscreen" hidden>⛶</button>
<script src="wasm_exec.js?v={{.Build}}"></script>
<script src="loader.js?v={{.Build}}"></script>
</body>

Some files were not shown because too many files have changed in this diff Show More