Add FxNode render graph playground

Add persistent render graph loadouts, grouped node authoring, code-appending apply behavior, explicit playground lighting, and resilient worker startup.

Amp-Thread-ID: https://ampcode.com/threads/T-01a033d4-0e44-708e-af0e-6166283416ff
Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
Amp
2026-08-24 17:45:48 +00:00
co-authored by heaust
parent 951449db0b
commit 6153223fd5
27 changed files with 4672 additions and 207 deletions
+60 -13
View File
@@ -4,6 +4,7 @@ import (
"context"
"crypto/rand"
"database/sql"
"encoding/json"
"errors"
"fmt"
"math/big"
@@ -17,18 +18,20 @@ import (
)
const (
maximumCodeBytes = 200 * 1024
maximumTitleRunes = 80
maximumCodeBytes = 200 * 1024
maximumLoadoutsBytes = 512 * 1024
maximumTitleRunes = 80
)
var playgroundIDPattern = regexp.MustCompile(`^[a-z0-9]{6,24}$`)
type playground struct {
ID string `json:"id"`
Revision int `json:"revision"`
Title string `json:"title"`
Code string `json:"code"`
CreatedAt time.Time `json:"createdAt"`
ID string `json:"id"`
Revision int `json:"revision"`
Title string `json:"title"`
Code string `json:"code"`
Loadouts json.RawMessage `json:"loadouts"`
CreatedAt time.Time `json:"createdAt"`
}
type playgroundStore struct {
@@ -61,16 +64,45 @@ func (store *playgroundStore) initialize() error {
revision INTEGER NOT NULL CHECK (revision > 0),
title TEXT NOT NULL,
code TEXT NOT NULL,
loadouts TEXT NOT NULL DEFAULT '[]',
created_at TEXT NOT NULL,
PRIMARY KEY (id, revision)
);
CREATE INDEX IF NOT EXISTS playgrounds_created_at
ON playgrounds (created_at DESC);
`)
if err != nil {
return err
}
rows, err := store.Query(`PRAGMA table_info(playgrounds)`)
if err != nil {
return err
}
hasLoadouts := false
for rows.Next() {
var position, notNull, primaryKey int
var name, kind string
var defaultValue any
if err := rows.Scan(&position, &name, &kind, &notNull, &defaultValue, &primaryKey); err != nil {
rows.Close()
return err
}
hasLoadouts = hasLoadouts || name == "loadouts"
}
if err := rows.Err(); err != nil {
rows.Close()
return err
}
if err := rows.Close(); err != nil {
return err
}
if !hasLoadouts {
_, err = store.Exec(`ALTER TABLE playgrounds ADD COLUMN loadouts TEXT NOT NULL DEFAULT '[]'`)
}
return err
}
func (store *playgroundStore) save(ctx context.Context, id, title, code string) (playground, error) {
func (store *playgroundStore) save(ctx context.Context, id, title, code, loadouts string) (playground, error) {
if id != "" && !playgroundIDPattern.MatchString(id) {
return playground{}, errors.New("invalid playground id")
}
@@ -80,6 +112,16 @@ func (store *playgroundStore) save(ctx context.Context, id, title, code string)
if code == "" || len(code) > maximumCodeBytes || !utf8.ValidString(code) {
return playground{}, fmt.Errorf("code must contain 1 to %d bytes of UTF-8", maximumCodeBytes)
}
if loadouts == "" {
loadouts = "[]"
}
if len(loadouts) > maximumLoadoutsBytes || !utf8.ValidString(loadouts) {
return playground{}, fmt.Errorf("loadouts must contain at most %d bytes of UTF-8", maximumLoadoutsBytes)
}
var loadoutList []json.RawMessage
if err := json.Unmarshal([]byte(loadouts), &loadoutList); err != nil || loadoutList == nil {
return playground{}, errors.New("loadouts must be a JSON array")
}
if id == "" {
var err error
id, err = randomPlaygroundID()
@@ -105,11 +147,12 @@ func (store *playgroundStore) save(ctx context.Context, id, title, code string)
createdAt := time.Now().UTC().Truncate(time.Millisecond)
if _, err := transaction.ExecContext(
ctx,
`INSERT INTO playgrounds (id, revision, title, code, created_at) VALUES (?, ?, ?, ?, ?)`,
`INSERT INTO playgrounds (id, revision, title, code, loadouts, created_at) VALUES (?, ?, ?, ?, ?, ?)`,
id,
revision,
title,
code,
loadouts,
createdAt.Format(time.RFC3339Nano),
); err != nil {
return playground{}, err
@@ -117,7 +160,10 @@ func (store *playgroundStore) save(ctx context.Context, id, title, code string)
if err := transaction.Commit(); err != nil {
return playground{}, err
}
return playground{ID: id, Revision: revision, Title: title, Code: code, CreatedAt: createdAt}, nil
return playground{
ID: id, Revision: revision, Title: title, Code: code,
Loadouts: json.RawMessage(loadouts), CreatedAt: createdAt,
}, nil
}
func (store *playgroundStore) get(ctx context.Context, id string, revision int) (playground, error) {
@@ -125,16 +171,17 @@ func (store *playgroundStore) get(ctx context.Context, id string, revision int)
return playground{}, sql.ErrNoRows
}
var result playground
var createdAt string
var createdAt, loadouts string
err := store.QueryRowContext(
ctx,
`SELECT id, revision, title, code, created_at FROM playgrounds WHERE id = ? AND revision = ?`,
`SELECT id, revision, title, code, loadouts, created_at FROM playgrounds WHERE id = ? AND revision = ?`,
id,
revision,
).Scan(&result.ID, &result.Revision, &result.Title, &result.Code, &createdAt)
).Scan(&result.ID, &result.Revision, &result.Title, &result.Code, &loadouts, &createdAt)
if err != nil {
return playground{}, err
}
result.Loadouts = json.RawMessage(loadouts)
result.CreatedAt, err = time.Parse(time.RFC3339Nano, createdAt)
return result, err
}