From 460b05f1875147a3469d37aaafcdc7149307d5d4 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Fri, 6 Nov 2026 12:38:45 +0800 Subject: [PATCH] feat(canvas): support unframed image snapshot nodes --- README.md | 5 +++++ internal/platform/canvas.go | 4 +++- internal/platform/canvas_test.go | 17 +++++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a16a6e8..c9f68b9 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,11 @@ serialize autosaves and handle 412 explicitly. Restore node style dimensions from width/height and render edges as Bézier curves. These frontend changes are outside this Core delivery; local drafts alone are not database persistence. +Canvas schema 1 supports unframed images: `type: "plainImage"` with +`data.kind: "plain-image"`. They require the same validated `data.src` as image +cards, support geometry/rotation/frame membership, and cannot connect to edges. +This is a snapshot-format extension with no additional migration. + Canvas schema 1 also supports frames: `type: "frame"` with `data.kind: "frame"`. Frame names use `data.title`; optional `data.labelMode` is `hidden`, `fixed`, or `scaled` (frontend default: `fixed`). Frames cannot rotate, connect to edges, diff --git a/internal/platform/canvas.go b/internal/platform/canvas.go index 4b4a404..2f235f0 100644 --- a/internal/platform/canvas.go +++ b/internal/platform/canvas.go @@ -135,6 +135,8 @@ func normalizeCanvas(raw json.RawMessage) (json.RawMessage, bool) { typ := "material" if kind == "plain-text" { typ = "plainText" + } else if kind == "plain-image" { + typ = "plainImage" } else if kind == "icon" { typ = "icon" } else if kind == "frame" { @@ -172,7 +174,7 @@ func normalizeCanvas(raw json.RawMessage) (json.RawMessage, bool) { return nil, false } } - if src, exists := d["src"]; (exists || kind == "image") && !canvasImage(src) { + if src, exists := d["src"]; (exists || kind == "image" || kind == "plain-image") && !canvasImage(src) { return nil, false } if value, exists := d["textStyle"]; exists { diff --git a/internal/platform/canvas_test.go b/internal/platform/canvas_test.go index e299d97..b8c3d1e 100644 --- a/internal/platform/canvas_test.go +++ b/internal/platform/canvas_test.go @@ -88,3 +88,20 @@ func TestCanvasFrames(t *testing.T) { t.Fatal("optional labelMode rejected") } } + +func TestCanvasPlainImages(t *testing.T) { + fixture := `{"schema_version":1,"nodes":[{"id":"photo","type":"plainImage","position":{"x":20,"y":30},"width":320,"height":240,"data":{"kind":"plain-image","title":"Photo","text":"","src":"https://example.com/photo.png","rotation":30,"frameId":"frame"}},{"id":"frame","type":"frame","position":{"x":0,"y":0},"width":640,"height":400,"data":{"kind":"frame","title":"Frame","text":"","labelMode":"fixed"}}],"edges":[],"viewport":{"x":0,"y":0,"zoom":1}}` + if _, ok := normalizeCanvas(json.RawMessage(fixture)); !ok { + t.Fatal("plain image rejected") + } + for _, change := range [][2]string{ + {`"type":"plainImage"`, `"type":"material"`}, + {`,"src":"https://example.com/photo.png"`, ``}, + {`"src":"https://example.com/photo.png"`, `"src":"javascript:bad"`}, + {`"edges":[]`, `"edges":[{"id":"e","source":"photo","target":"photo"}]`}, + } { + if _, ok := normalizeCanvas(json.RawMessage(strings.Replace(fixture, change[0], change[1], 1))); ok { + t.Fatal("invalid pure image accepted") + } + } +} -- 2.52.0