Files
foundry-core/internal/platform/canvas_v2.go
T
codex-bot d6ee8942f1
Foundry Go checks / check (push) Successful in 2m40s
Foundry Go checks / check (pull_request) Successful in 2m36s
Implement Canvas V2 subgraphs, assets and asynchronous AI execution
2026-11-06 18:36:00 +08:00

176 lines
4.4 KiB
Go

package platform
import (
"encoding/json"
"math"
"mime"
"net/http"
"strconv"
"strings"
)
func canvasSchema(raw json.RawMessage) int {
var d struct {
Version int `json:"schema_version"`
}
_ = json.Unmarshal(raw, &d)
return d.Version
}
func acceptsCanvasV2(r *http.Request) bool {
for _, value := range strings.Split(r.Header.Get("Accept"), ",") {
typ, params, err := mime.ParseMediaType(strings.TrimSpace(value))
if err == nil && typ == "application/vnd.foundry.canvas+json" && params["version"] == "2" {
if value, exists := params["q"]; exists {
q, e := strconv.ParseFloat(value, 64)
if e != nil || !(q > 0 && q <= 1) {
continue
}
}
return true
}
}
return false
}
func canvasPoint(v any) bool {
p, ok := v.(map[string]any)
return ok && canvasFields(p, "x", "y") && canvasNumber(p["x"], -1e6, 1e6) && canvasNumber(p["y"], -1e6, 1e6)
}
func validCanvasEdgeData(v any) bool {
d, ok := v.(map[string]any)
if !ok || !canvasFields(d, "appearance", "animation", "path", "execution") {
return false
}
for key, value := range d {
m, ok := value.(map[string]any)
if !ok {
return false
}
switch key {
case "appearance":
if !canvasFields(m, "color", "width", "opacity", "line", "head", "tail") {
return false
}
for k, v := range m {
switch k {
case "color":
s, ok := v.(string)
if !ok || !canvasColor.MatchString(s) {
return false
}
case "width":
if !canvasNumber(v, .5, 20) {
return false
}
case "opacity":
if !canvasNumber(v, .05, 1) {
return false
}
case "line":
if !canvasChoice(v, "solid", "dashed", "dotted") {
return false
}
case "head", "tail":
marker, ok := v.(map[string]any)
if !ok || !canvasFields(marker, "type", "size", "filled") || !canvasChoice(marker["type"], "none", "open-arrow", "triangle", "circle", "diamond", "square") {
return false
}
if v, exists := marker["size"]; exists && !canvasNumber(v, 4, 32) {
return false
}
if v, exists := marker["filled"]; exists {
if _, ok := v.(bool); !ok {
return false
}
}
}
}
case "animation":
if !canvasFields(m, "mode", "effect", "direction", "durationMs", "color") {
return false
}
for k, v := range m {
switch k {
case "mode":
if !canvasChoice(v, "off", "always", "executing") {
return false
}
case "effect":
if !canvasChoice(v, "flow", "dash", "pulse") {
return false
}
case "direction":
if !canvasChoice(v, "forward", "reverse") {
return false
}
case "durationMs":
if !canvasNumber(v, 250, 30000) {
return false
}
case "color":
s, ok := v.(string)
if !ok || !canvasColor.MatchString(s) {
return false
}
}
}
case "execution":
if !canvasFields(m, "enabled") {
return false
}
if v, exists := m["enabled"]; exists {
if _, ok := v.(bool); !ok {
return false
}
}
case "path":
if !canvasFields(m, "type", "startControl", "endControl", "knots") || m["type"] != "bezier-chain" {
return false
}
for _, k := range []string{"startControl", "endControl"} {
if v, exists := m[k]; exists && !canvasPoint(v) {
return false
}
}
knots, ok := m["knots"].([]any)
if !ok || len(knots) > 64 {
return false
}
seen := map[string]bool{}
for _, v := range knots {
knot, ok := v.(map[string]any)
if !ok || !canvasFields(knot, "id", "position", "in", "out", "mode") {
return false
}
id, ok := knot["id"].(string)
if !ok || !canvasID.MatchString(id) || seen[id] {
return false
}
seen[id] = true
for _, k := range []string{"position", "in", "out"} {
if !canvasPoint(knot[k]) {
return false
}
}
if !canvasChoice(knot["mode"], "corner", "smooth", "symmetric") {
return false
}
if knot["mode"] != "corner" {
a, b := knot["in"].(map[string]any), knot["out"].(map[string]any)
ax, ay, bx, by := a["x"].(float64), a["y"].(float64), b["x"].(float64), b["y"].(float64)
la, lb := math.Hypot(ax, ay), math.Hypot(bx, by)
if la > 0 && lb > 0 && (math.Abs((ax/la)*(by/lb)-(ay/la)*(bx/lb)) > 1e-6 || (ax/la)*(bx/lb)+(ay/la)*(by/lb) > 0) {
return false
}
if knot["mode"] == "symmetric" && math.Abs(la-lb) > 1e-6*math.Max(la, lb) {
return false
}
}
}
}
}
return true
}