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:
+60
-13
@@ -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, ¬Null, &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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user