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)
266 lines
5.2 KiB
Go
266 lines
5.2 KiB
Go
package clip
|
|
|
|
import (
|
|
"bytes"
|
|
"image"
|
|
"image/draw"
|
|
_ "image/gif"
|
|
_ "image/jpeg"
|
|
"image/png"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"aego/platform"
|
|
)
|
|
|
|
const (
|
|
MimePNG = "image/png"
|
|
MimeBMP = "image/bmp"
|
|
MimeXBMP = "image/x-bmp"
|
|
MimeURIList = "text/uri-list"
|
|
MimeGnome = "x-special/gnome-copied-files"
|
|
MimeText = "text/plain;charset=utf-8"
|
|
MimeTextPlain = "text/plain"
|
|
maxTextPaths = 64
|
|
maxPathText = 1 << 16
|
|
)
|
|
|
|
type Item = platform.ClipItem
|
|
|
|
type Reader interface {
|
|
Text() string
|
|
Data(mime string) ([]byte, bool)
|
|
}
|
|
|
|
type Writer interface {
|
|
SetText(s string)
|
|
SetData(mime string, data []byte, text string) bool
|
|
}
|
|
|
|
type Prober interface {
|
|
Has(mime string) bool
|
|
}
|
|
|
|
type MultiWriter interface {
|
|
SetItems(items []Item) bool
|
|
}
|
|
|
|
var imageMimes = [...]string{MimePNG, MimeBMP, MimeXBMP}
|
|
|
|
func Has(r Reader, mime string) bool {
|
|
if r == nil {
|
|
return false
|
|
}
|
|
if p, ok := r.(Prober); ok {
|
|
return p.Has(mime)
|
|
}
|
|
data, ok := r.Data(mime)
|
|
return ok && len(data) > 0
|
|
}
|
|
|
|
func maybe(r Reader, mime string) bool {
|
|
if p, ok := r.(Prober); ok {
|
|
return p.Has(mime)
|
|
}
|
|
return true
|
|
}
|
|
|
|
func HasImage(r Reader) bool {
|
|
for _, m := range imageMimes {
|
|
if Has(r, m) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func HasFiles(r Reader) bool {
|
|
return Has(r, MimeURIList) || Has(r, MimeGnome)
|
|
}
|
|
|
|
func DecodeImage(data []byte) (*image.NRGBA, error) {
|
|
if len(data) >= 2 && data[0] == 'B' && data[1] == 'M' {
|
|
return DecodeBMP(data)
|
|
}
|
|
img, _, err := image.Decode(bytes.NewReader(data))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return ToNRGBA(img), nil
|
|
}
|
|
|
|
func ToNRGBA(img image.Image) *image.NRGBA {
|
|
if n, ok := img.(*image.NRGBA); ok && n.Rect.Min == (image.Point{}) {
|
|
return n
|
|
}
|
|
b := img.Bounds()
|
|
out := image.NewNRGBA(image.Rect(0, 0, b.Dx(), b.Dy()))
|
|
draw.Draw(out, out.Rect, img, b.Min, draw.Src)
|
|
return out
|
|
}
|
|
|
|
func EncodePNG(img image.Image) []byte {
|
|
var buf bytes.Buffer
|
|
if err := png.Encode(&buf, img); err != nil {
|
|
return nil
|
|
}
|
|
return buf.Bytes()
|
|
}
|
|
|
|
func Image(r Reader) (*image.NRGBA, bool) {
|
|
if r == nil {
|
|
return nil, false
|
|
}
|
|
for _, m := range imageMimes {
|
|
if !maybe(r, m) {
|
|
continue
|
|
}
|
|
data, ok := r.Data(m)
|
|
if !ok || len(data) == 0 {
|
|
continue
|
|
}
|
|
if img, err := DecodeImage(data); err == nil {
|
|
return img, true
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
func Files(r Reader) ([]string, bool) {
|
|
if r == nil {
|
|
return nil, false
|
|
}
|
|
for _, m := range [...]string{MimeURIList, MimeGnome} {
|
|
if !maybe(r, m) {
|
|
continue
|
|
}
|
|
if data, ok := r.Data(m); ok {
|
|
if files := ParseURIList(data); len(files) > 0 {
|
|
return files, true
|
|
}
|
|
}
|
|
}
|
|
return textPaths(r.Text())
|
|
}
|
|
|
|
func textPaths(s string) ([]string, bool) {
|
|
s = strings.TrimSpace(s)
|
|
if s == "" || len(s) > maxPathText {
|
|
return nil, false
|
|
}
|
|
var out []string
|
|
for _, line := range strings.Split(s, "\n") {
|
|
line = strings.Trim(strings.TrimSpace(line), `"'`)
|
|
if line == "" {
|
|
continue
|
|
}
|
|
if p, ok := fromURI(line); ok {
|
|
line = p
|
|
}
|
|
if !filepath.IsAbs(line) || len(out) == maxTextPaths {
|
|
return nil, false
|
|
}
|
|
if _, err := os.Stat(line); err != nil {
|
|
return nil, false
|
|
}
|
|
out = append(out, filepath.Clean(line))
|
|
}
|
|
return out, len(out) > 0
|
|
}
|
|
|
|
func fromURI(s string) (string, bool) {
|
|
u, err := url.Parse(s)
|
|
if err != nil || u.Scheme != "file" || (u.Host != "" && u.Host != "localhost") {
|
|
return "", false
|
|
}
|
|
p := u.Path
|
|
if runtime.GOOS == "windows" && len(p) > 2 && p[0] == '/' && p[2] == ':' {
|
|
p = p[1:]
|
|
}
|
|
if p == "" {
|
|
return "", false
|
|
}
|
|
return filepath.FromSlash(p), true
|
|
}
|
|
|
|
func toURI(p string) string {
|
|
s := filepath.ToSlash(p)
|
|
if !strings.HasPrefix(s, "/") {
|
|
s = "/" + s
|
|
}
|
|
return (&url.URL{Scheme: "file", Path: s}).String()
|
|
}
|
|
|
|
func ParseURIList(data []byte) []string {
|
|
var out []string
|
|
for _, line := range strings.Split(string(data), "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" || line[0] == '#' {
|
|
continue
|
|
}
|
|
if p, ok := fromURI(line); ok {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func URIList(paths []string) []byte {
|
|
var b strings.Builder
|
|
for _, p := range paths {
|
|
b.WriteString(toURI(p))
|
|
b.WriteString("\r\n")
|
|
}
|
|
return []byte(b.String())
|
|
}
|
|
|
|
func GnomeFiles(paths []string) []byte {
|
|
parts := []string{"copy"}
|
|
for _, p := range paths {
|
|
parts = append(parts, toURI(p))
|
|
}
|
|
return []byte(strings.Join(parts, "\n"))
|
|
}
|
|
|
|
func fileItems(paths []string) []Item {
|
|
if len(paths) == 0 {
|
|
return nil
|
|
}
|
|
text := []byte(strings.Join(paths, "\n"))
|
|
return []Item{
|
|
{Mime: MimeURIList, Data: URIList(paths)},
|
|
{Mime: MimeGnome, Data: GnomeFiles(paths)},
|
|
{Mime: MimeText, Data: text},
|
|
{Mime: MimeTextPlain, Data: text},
|
|
}
|
|
}
|
|
|
|
func put(w Writer, items []Item, text string) bool {
|
|
if w == nil || len(items) == 0 {
|
|
return false
|
|
}
|
|
if m, ok := w.(MultiWriter); ok {
|
|
return m.SetItems(items)
|
|
}
|
|
return w.SetData(items[0].Mime, items[0].Data, text)
|
|
}
|
|
|
|
func SetFiles(w Writer, paths []string) bool {
|
|
return put(w, fileItems(paths), strings.Join(paths, "\n"))
|
|
}
|
|
|
|
func SetImagePNG(w Writer, data []byte, paths []string) bool {
|
|
if len(data) == 0 {
|
|
return false
|
|
}
|
|
items := append([]Item{{Mime: MimePNG, Data: data}}, fileItems(paths)...)
|
|
return put(w, items, strings.Join(paths, "\n"))
|
|
}
|
|
|
|
func SetImage(w Writer, img image.Image, paths []string) bool {
|
|
return SetImagePNG(w, EncodePNG(img), paths)
|
|
}
|