diff --git a/server/API.md b/server/API.md index 61e3c36..9fc9d14 100644 --- a/server/API.md +++ b/server/API.md @@ -419,6 +419,8 @@ Subsequent shared project requests send `X-Share-Id: `. Middleware re-r The harness builds short memory from the current turn, recent messages, and visible canvas state, and long memory from durable project decisions, prior artifacts, and previous research records with page-content excerpts. Image titles and assistant language are generated from the request context instead of fixed model labels. +CAD references may include a text content part with `textSource: "cad-context"`. It is retained in agent planning, memory, and image-generation prompts but omitted from persisted user-visible message copy. The context is bounded and treats CAD labels as reference data, never as executable instructions. + ## Generate `POST /api/projects/:id/generate` @@ -537,6 +539,8 @@ Returns `{ "code": 0, "msg": "success", "data": { "status": "done" } }` or the c Saves canvas layout changes with a Lovart-style lightweight payload. The frontend posts to `/api/canva/project/saveProject`; `/api/projects/:id/canvas` remains as a compatibility route. The `canvas` value is a `SHAKKERDATA://` gzip+base64 encoded snapshot containing viewport, nodes, and connections. Legacy uncompressed `{ "viewport": ..., "nodes": ..., "connections": ... }` payloads are still accepted. +Canvas nodes may include optional `semanticContext`. CAD source nodes use `layerRole: "cad-vector"`; non-destructive crop siblings use `layerRole: "cad-crop"` and carry crop-specific context. PostgreSQL persists the field in `canvas_nodes.semantic_context`. + When a canvas image node disappears from the saved node list, its backing OSS asset is queued for asynchronous deletion after the canvas save succeeds. ## Image Asset Upload diff --git a/server/img_infinite_canvas.api b/server/img_infinite_canvas.api index b57a8e5..e7f10c6 100644 --- a/server/img_infinite_canvas.api +++ b/server/img_infinite_canvas.api @@ -242,6 +242,7 @@ type CanvasNode { Status string `json:"status"` ParentId string `json:"parentId,optional"` LayerRole string `json:"layerRole,optional"` + SemanticContext string `json:"semanticContext,optional"` FontSize float64 `json:"fontSize,optional"` FontFamily string `json:"fontFamily,optional"` FontWeight string `json:"fontWeight,optional"` diff --git a/server/internal/application/cad_context_test.go b/server/internal/application/cad_context_test.go new file mode 100644 index 0000000..5696e8a --- /dev/null +++ b/server/internal/application/cad_context_test.go @@ -0,0 +1,43 @@ +package application + +import ( + "strings" + "testing" + + "img_infinite_canvas/internal/domain/design" +) + +func TestCADContextIsModelInputButNotVisibleUserCopy(t *testing.T) { + messages := []design.AgentChatMessage{ + { + Role: "user", + Contents: []design.AgentContent{ + {Type: "text", Text: "Focus on the living room", TextSource: "input"}, + {Type: "image", ImageURL: "/canvas-assets/floor-plan.webp", ImageName: "floor-plan crop.webp"}, + {Type: "text", Text: "CAD semantic context: layer ROOMS, block ROOM_A", TextSource: "cad-context"}, + }, + }, + } + + modelPrompt := promptFromAgentMessages(messages) + visiblePrompt := visiblePromptFromAgentMessages(messages) + if !strings.Contains(modelPrompt, "layer ROOMS") { + t.Fatalf("expected model prompt to retain CAD semantics, got %q", modelPrompt) + } + if strings.Contains(visiblePrompt, "layer ROOMS") { + t.Fatalf("expected visible user copy to hide CAD semantics, got %q", visiblePrompt) + } + if !strings.Contains(visiblePrompt, "Focus on the living room") || !strings.Contains(visiblePrompt, "floor-plan crop.webp") { + t.Fatalf("expected visible prompt and reference capsule directive, got %q", visiblePrompt) + } +} + +func TestCADContextAloneDoesNotSatisfyPromptValidation(t *testing.T) { + messages := []design.AgentChatMessage{{ + Role: "user", + Contents: []design.AgentContent{{Type: "text", Text: "CAD semantic context", TextSource: "cad-context"}}, + }} + if hasUserPromptTextInAgentMessages(messages) { + t.Fatal("expected hidden CAD context not to count as user prompt text") + } +} diff --git a/server/internal/application/design_service.go b/server/internal/application/design_service.go index 4291710..3c4a90a 100644 --- a/server/internal/application/design_service.go +++ b/server/internal/application/design_service.go @@ -669,6 +669,10 @@ func (s *DesignService) AgentChat(ctx context.Context, projectID string, req des if prompt == "" { return design.AgentThread{}, fmt.Errorf("%w: prompt is required", design.ErrInvalidInput) } + visiblePrompt := design.NormalizePrompt(visiblePromptFromAgentMessages(req.Messages)) + if visiblePrompt == "" { + visiblePrompt = prompt + } mode := strings.TrimSpace(req.Mode) if mode == "" { @@ -703,7 +707,7 @@ func (s *DesignService) AgentChat(ctx context.Context, projectID string, req des Role: "user", Type: "user", Title: "新的设计需求", - Content: prompt, + Content: visiblePrompt, ThreadID: threadID, ActionID: actionID, StepID: newID(), @@ -5153,6 +5157,14 @@ func isGeneratedImageContent(value string) bool { } func promptFromAgentMessages(messages []design.AgentChatMessage) string { + return promptFromAgentMessagesWithVisibility(messages, false) +} + +func visiblePromptFromAgentMessages(messages []design.AgentChatMessage) string { + return promptFromAgentMessagesWithVisibility(messages, true) +} + +func promptFromAgentMessagesWithVisibility(messages []design.AgentChatMessage, visibleOnly bool) string { var builder strings.Builder for _, message := range messages { if strings.TrimSpace(message.Role) != "" && strings.ToLower(strings.TrimSpace(message.Role)) != "user" { @@ -5161,7 +5173,8 @@ func promptFromAgentMessages(messages []design.AgentChatMessage) string { for _, content := range message.Contents { switch strings.ToLower(strings.TrimSpace(content.Type)) { case "text": - if strings.EqualFold(strings.TrimSpace(content.TextSource), "settings") { + textSource := strings.TrimSpace(content.TextSource) + if strings.EqualFold(textSource, "settings") || (visibleOnly && strings.EqualFold(textSource, "cad-context")) { continue } if text := strings.TrimSpace(content.Text); text != "" { @@ -5207,7 +5220,8 @@ func hasUserPromptTextInAgentMessages(messages []design.AgentChatMessage) bool { if strings.ToLower(strings.TrimSpace(content.Type)) != "text" { continue } - if strings.EqualFold(strings.TrimSpace(content.TextSource), "settings") { + textSource := strings.TrimSpace(content.TextSource) + if strings.EqualFold(textSource, "settings") || strings.EqualFold(textSource, "cad-context") { continue } text := stripPromptReferenceDirectives(stripImageGeneratorDirectiveLines(content.Text)) diff --git a/server/internal/domain/design/project.go b/server/internal/domain/design/project.go index 147a953..ed8d224 100644 --- a/server/internal/domain/design/project.go +++ b/server/internal/domain/design/project.go @@ -134,6 +134,7 @@ type Node struct { Status string ParentID string LayerRole string + SemanticContext string FontSize float64 FontFamily string FontWeight string diff --git a/server/internal/infrastructure/postgres/db/models.go b/server/internal/infrastructure/postgres/db/models.go index 01f6c53..7ff1785 100644 --- a/server/internal/infrastructure/postgres/db/models.go +++ b/server/internal/infrastructure/postgres/db/models.go @@ -26,6 +26,20 @@ type AgentMessage struct { CreatedAt pgtype.Timestamptz `json:"created_at"` } +type AuthDeviceSession struct { + ID string `json:"id"` + UserID pgtype.UUID `json:"user_id"` + DeviceType string `json:"device_type"` + System string `json:"system"` + Browser string `json:"browser"` + Client string `json:"client"` + UserAgent string `json:"user_agent"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + LastSeenAt pgtype.Timestamptz `json:"last_seen_at"` + ExpiresAt pgtype.Timestamptz `json:"expires_at"` + RevokedAt pgtype.Timestamptz `json:"revoked_at"` +} + type AuthVerificationCode struct { ID string `json:"id"` Region string `json:"region"` @@ -58,38 +72,39 @@ type CanvasConnection struct { } type CanvasNode struct { - ID string `json:"id"` - ProjectID string `json:"project_id"` - UserID pgtype.UUID `json:"user_id"` - NodeType string `json:"node_type"` - Title string `json:"title"` - X float64 `json:"x"` - Y float64 `json:"y"` - Width float64 `json:"width"` - Height float64 `json:"height"` - Content string `json:"content"` - Tone string `json:"tone"` - Status string `json:"status"` - SortOrder int32 `json:"sort_order"` - ParentID string `json:"parent_id"` - LayerRole string `json:"layer_role"` - FontSize float64 `json:"font_size"` - FontFamily string `json:"font_family"` - FontWeight string `json:"font_weight"` - Color string `json:"color"` - TextAlign string `json:"text_align"` - LineHeight float64 `json:"line_height"` - LetterSpacing float64 `json:"letter_spacing"` - TextDecoration string `json:"text_decoration"` - TextTransform string `json:"text_transform"` - Opacity float64 `json:"opacity"` - FillColor string `json:"fill_color"` - StrokeColor string `json:"stroke_color"` - StrokeWidth float64 `json:"stroke_width"` - StrokeStyle string `json:"stroke_style"` - FlipX bool `json:"flip_x"` - FlipY bool `json:"flip_y"` - Rotation float64 `json:"rotation"` + ID string `json:"id"` + ProjectID string `json:"project_id"` + UserID pgtype.UUID `json:"user_id"` + NodeType string `json:"node_type"` + Title string `json:"title"` + X float64 `json:"x"` + Y float64 `json:"y"` + Width float64 `json:"width"` + Height float64 `json:"height"` + Content string `json:"content"` + Tone string `json:"tone"` + Status string `json:"status"` + SortOrder int32 `json:"sort_order"` + ParentID string `json:"parent_id"` + LayerRole string `json:"layer_role"` + FontSize float64 `json:"font_size"` + FontFamily string `json:"font_family"` + FontWeight string `json:"font_weight"` + Color string `json:"color"` + TextAlign string `json:"text_align"` + LineHeight float64 `json:"line_height"` + LetterSpacing float64 `json:"letter_spacing"` + TextDecoration string `json:"text_decoration"` + TextTransform string `json:"text_transform"` + Opacity float64 `json:"opacity"` + FillColor string `json:"fill_color"` + StrokeColor string `json:"stroke_color"` + StrokeWidth float64 `json:"stroke_width"` + StrokeStyle string `json:"stroke_style"` + FlipX bool `json:"flip_x"` + FlipY bool `json:"flip_y"` + Rotation float64 `json:"rotation"` + SemanticContext string `json:"semantic_context"` } type CanvasSnapshot struct { diff --git a/server/internal/infrastructure/postgres/db/query.sql.go b/server/internal/infrastructure/postgres/db/query.sql.go index 3694b0e..14e2f75 100644 --- a/server/internal/infrastructure/postgres/db/query.sql.go +++ b/server/internal/infrastructure/postgres/db/query.sql.go @@ -467,7 +467,7 @@ func (q *Queries) ListMessagesByProject(ctx context.Context, arg ListMessagesByP } const listNodesByProject = `-- name: ListNodesByProject :many -SELECT id, project_id, user_id, node_type, title, x, y, width, height, content, tone, status, sort_order, parent_id, layer_role, font_size, font_family, font_weight, color, text_align, line_height, letter_spacing, text_decoration, text_transform, opacity, fill_color, stroke_color, stroke_width, stroke_style, flip_x, flip_y, rotation +SELECT id, project_id, user_id, node_type, title, x, y, width, height, content, tone, status, sort_order, parent_id, layer_role, font_size, font_family, font_weight, color, text_align, line_height, letter_spacing, text_decoration, text_transform, opacity, fill_color, stroke_color, stroke_width, stroke_style, flip_x, flip_y, rotation, semantic_context FROM canvas_nodes WHERE project_id = $1 AND user_id = $2 ORDER BY sort_order ASC, id ASC @@ -520,6 +520,7 @@ func (q *Queries) ListNodesByProject(ctx context.Context, arg ListNodesByProject &i.FlipX, &i.FlipY, &i.Rotation, + &i.SemanticContext, ); err != nil { return nil, err } @@ -745,8 +746,8 @@ func (q *Queries) UpsertBrandKit(ctx context.Context, arg UpsertBrandKitParams) } const upsertCanvasNode = `-- name: UpsertCanvasNode :exec -INSERT INTO canvas_nodes (id, project_id, user_id, node_type, title, x, y, width, height, content, tone, status, sort_order, parent_id, layer_role, font_size, font_family, font_weight, color, text_align, line_height, letter_spacing, text_decoration, text_transform, opacity, fill_color, stroke_color, stroke_width, stroke_style, flip_x, flip_y, rotation) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32) +INSERT INTO canvas_nodes (id, project_id, user_id, node_type, title, x, y, width, height, content, tone, status, sort_order, parent_id, layer_role, font_size, font_family, font_weight, color, text_align, line_height, letter_spacing, text_decoration, text_transform, opacity, fill_color, stroke_color, stroke_width, stroke_style, flip_x, flip_y, rotation, semantic_context) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33) ON CONFLICT (id) DO UPDATE SET project_id = EXCLUDED.project_id, user_id = EXCLUDED.user_id, @@ -778,42 +779,44 @@ ON CONFLICT (id) DO UPDATE SET stroke_style = EXCLUDED.stroke_style, flip_x = EXCLUDED.flip_x, flip_y = EXCLUDED.flip_y, - rotation = EXCLUDED.rotation + rotation = EXCLUDED.rotation, + semantic_context = EXCLUDED.semantic_context ` type UpsertCanvasNodeParams struct { - ID string `json:"id"` - ProjectID string `json:"project_id"` - UserID pgtype.UUID `json:"user_id"` - NodeType string `json:"node_type"` - Title string `json:"title"` - X float64 `json:"x"` - Y float64 `json:"y"` - Width float64 `json:"width"` - Height float64 `json:"height"` - Content string `json:"content"` - Tone string `json:"tone"` - Status string `json:"status"` - SortOrder int32 `json:"sort_order"` - ParentID string `json:"parent_id"` - LayerRole string `json:"layer_role"` - FontSize float64 `json:"font_size"` - FontFamily string `json:"font_family"` - FontWeight string `json:"font_weight"` - Color string `json:"color"` - TextAlign string `json:"text_align"` - LineHeight float64 `json:"line_height"` - LetterSpacing float64 `json:"letter_spacing"` - TextDecoration string `json:"text_decoration"` - TextTransform string `json:"text_transform"` - Opacity float64 `json:"opacity"` - FillColor string `json:"fill_color"` - StrokeColor string `json:"stroke_color"` - StrokeWidth float64 `json:"stroke_width"` - StrokeStyle string `json:"stroke_style"` - FlipX bool `json:"flip_x"` - FlipY bool `json:"flip_y"` - Rotation float64 `json:"rotation"` + ID string `json:"id"` + ProjectID string `json:"project_id"` + UserID pgtype.UUID `json:"user_id"` + NodeType string `json:"node_type"` + Title string `json:"title"` + X float64 `json:"x"` + Y float64 `json:"y"` + Width float64 `json:"width"` + Height float64 `json:"height"` + Content string `json:"content"` + Tone string `json:"tone"` + Status string `json:"status"` + SortOrder int32 `json:"sort_order"` + ParentID string `json:"parent_id"` + LayerRole string `json:"layer_role"` + FontSize float64 `json:"font_size"` + FontFamily string `json:"font_family"` + FontWeight string `json:"font_weight"` + Color string `json:"color"` + TextAlign string `json:"text_align"` + LineHeight float64 `json:"line_height"` + LetterSpacing float64 `json:"letter_spacing"` + TextDecoration string `json:"text_decoration"` + TextTransform string `json:"text_transform"` + Opacity float64 `json:"opacity"` + FillColor string `json:"fill_color"` + StrokeColor string `json:"stroke_color"` + StrokeWidth float64 `json:"stroke_width"` + StrokeStyle string `json:"stroke_style"` + FlipX bool `json:"flip_x"` + FlipY bool `json:"flip_y"` + Rotation float64 `json:"rotation"` + SemanticContext string `json:"semantic_context"` } func (q *Queries) UpsertCanvasNode(ctx context.Context, arg UpsertCanvasNodeParams) error { @@ -850,6 +853,7 @@ func (q *Queries) UpsertCanvasNode(ctx context.Context, arg UpsertCanvasNodePara arg.FlipX, arg.FlipY, arg.Rotation, + arg.SemanticContext, ) return err } diff --git a/server/internal/infrastructure/postgres/project_repository.go b/server/internal/infrastructure/postgres/project_repository.go index 8660de4..7886a33 100644 --- a/server/internal/infrastructure/postgres/project_repository.go +++ b/server/internal/infrastructure/postgres/project_repository.go @@ -189,38 +189,39 @@ func (r *ProjectRepository) Save(ctx context.Context, project design.Project) er for index, node := range project.Nodes { if err := q.UpsertCanvasNode(ctx, sqlcdb.UpsertCanvasNodeParams{ - ID: node.ID, - ProjectID: project.ID, - UserID: toPGUUID(project.UserID), - NodeType: string(node.Type), - Title: node.Title, - X: node.X, - Y: node.Y, - Width: node.Width, - Height: node.Height, - Content: node.Content, - Tone: node.Tone, - Status: node.Status, - SortOrder: int32(index), - ParentID: node.ParentID, - LayerRole: node.LayerRole, - FontSize: node.FontSize, - FontFamily: node.FontFamily, - FontWeight: node.FontWeight, - Color: node.Color, - TextAlign: node.TextAlign, - LineHeight: node.LineHeight, - LetterSpacing: node.LetterSpacing, - TextDecoration: node.TextDecoration, - TextTransform: node.TextTransform, - Opacity: node.Opacity, - FillColor: node.FillColor, - StrokeColor: node.StrokeColor, - StrokeWidth: node.StrokeWidth, - StrokeStyle: node.StrokeStyle, - FlipX: node.FlipX, - FlipY: node.FlipY, - Rotation: node.Rotation, + ID: node.ID, + ProjectID: project.ID, + UserID: toPGUUID(project.UserID), + NodeType: string(node.Type), + Title: node.Title, + X: node.X, + Y: node.Y, + Width: node.Width, + Height: node.Height, + Content: node.Content, + Tone: node.Tone, + Status: node.Status, + SortOrder: int32(index), + ParentID: node.ParentID, + LayerRole: node.LayerRole, + SemanticContext: node.SemanticContext, + FontSize: node.FontSize, + FontFamily: node.FontFamily, + FontWeight: node.FontWeight, + Color: node.Color, + TextAlign: node.TextAlign, + LineHeight: node.LineHeight, + LetterSpacing: node.LetterSpacing, + TextDecoration: node.TextDecoration, + TextTransform: node.TextTransform, + Opacity: node.Opacity, + FillColor: node.FillColor, + StrokeColor: node.StrokeColor, + StrokeWidth: node.StrokeWidth, + StrokeStyle: node.StrokeStyle, + FlipX: node.FlipX, + FlipY: node.FlipY, + Rotation: node.Rotation, }); err != nil { return err } @@ -509,35 +510,36 @@ func mapNodes(rows []sqlcdb.CanvasNode) []design.Node { nodes := make([]design.Node, 0, len(rows)) for _, row := range rows { nodes = append(nodes, design.Node{ - ID: row.ID, - Type: design.NodeType(row.NodeType), - Title: row.Title, - X: row.X, - Y: row.Y, - Width: row.Width, - Height: row.Height, - Content: row.Content, - Tone: row.Tone, - Status: row.Status, - ParentID: row.ParentID, - LayerRole: row.LayerRole, - FontSize: row.FontSize, - FontFamily: row.FontFamily, - FontWeight: row.FontWeight, - Color: row.Color, - TextAlign: row.TextAlign, - LineHeight: row.LineHeight, - LetterSpacing: row.LetterSpacing, - TextDecoration: row.TextDecoration, - TextTransform: row.TextTransform, - Opacity: row.Opacity, - FillColor: row.FillColor, - StrokeColor: row.StrokeColor, - StrokeWidth: row.StrokeWidth, - StrokeStyle: row.StrokeStyle, - FlipX: row.FlipX, - FlipY: row.FlipY, - Rotation: row.Rotation, + ID: row.ID, + Type: design.NodeType(row.NodeType), + Title: row.Title, + X: row.X, + Y: row.Y, + Width: row.Width, + Height: row.Height, + Content: row.Content, + Tone: row.Tone, + Status: row.Status, + ParentID: row.ParentID, + LayerRole: row.LayerRole, + SemanticContext: row.SemanticContext, + FontSize: row.FontSize, + FontFamily: row.FontFamily, + FontWeight: row.FontWeight, + Color: row.Color, + TextAlign: row.TextAlign, + LineHeight: row.LineHeight, + LetterSpacing: row.LetterSpacing, + TextDecoration: row.TextDecoration, + TextTransform: row.TextTransform, + Opacity: row.Opacity, + FillColor: row.FillColor, + StrokeColor: row.StrokeColor, + StrokeWidth: row.StrokeWidth, + StrokeStyle: row.StrokeStyle, + FlipX: row.FlipX, + FlipY: row.FlipY, + Rotation: row.Rotation, }) } return nodes diff --git a/server/internal/infrastructure/postgres/query.sql b/server/internal/infrastructure/postgres/query.sql index 892bd8d..2ce15e4 100644 --- a/server/internal/infrastructure/postgres/query.sql +++ b/server/internal/infrastructure/postgres/query.sql @@ -114,7 +114,7 @@ SET thumbnail = $3, WHERE id = $1 AND user_id = $2; -- name: ListNodesByProject :many -SELECT id, project_id, user_id, node_type, title, x, y, width, height, content, tone, status, sort_order, parent_id, layer_role, font_size, font_family, font_weight, color, text_align, line_height, letter_spacing, text_decoration, text_transform, opacity, fill_color, stroke_color, stroke_width, stroke_style, flip_x, flip_y, rotation +SELECT id, project_id, user_id, node_type, title, x, y, width, height, content, tone, status, sort_order, parent_id, layer_role, font_size, font_family, font_weight, color, text_align, line_height, letter_spacing, text_decoration, text_transform, opacity, fill_color, stroke_color, stroke_width, stroke_style, flip_x, flip_y, rotation, semantic_context FROM canvas_nodes WHERE project_id = $1 AND user_id = $2 ORDER BY sort_order ASC, id ASC; @@ -124,8 +124,8 @@ DELETE FROM canvas_nodes WHERE project_id = $1 AND user_id = $2; -- name: UpsertCanvasNode :exec -INSERT INTO canvas_nodes (id, project_id, user_id, node_type, title, x, y, width, height, content, tone, status, sort_order, parent_id, layer_role, font_size, font_family, font_weight, color, text_align, line_height, letter_spacing, text_decoration, text_transform, opacity, fill_color, stroke_color, stroke_width, stroke_style, flip_x, flip_y, rotation) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32) +INSERT INTO canvas_nodes (id, project_id, user_id, node_type, title, x, y, width, height, content, tone, status, sort_order, parent_id, layer_role, font_size, font_family, font_weight, color, text_align, line_height, letter_spacing, text_decoration, text_transform, opacity, fill_color, stroke_color, stroke_width, stroke_style, flip_x, flip_y, rotation, semantic_context) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33) ON CONFLICT (id) DO UPDATE SET project_id = EXCLUDED.project_id, user_id = EXCLUDED.user_id, @@ -157,7 +157,8 @@ ON CONFLICT (id) DO UPDATE SET stroke_style = EXCLUDED.stroke_style, flip_x = EXCLUDED.flip_x, flip_y = EXCLUDED.flip_y, - rotation = EXCLUDED.rotation; + rotation = EXCLUDED.rotation, + semantic_context = EXCLUDED.semantic_context; -- name: ListConnectionsByProject :many SELECT id, project_id, user_id, from_node_id, to_node_id diff --git a/server/internal/infrastructure/postgres/schema.sql b/server/internal/infrastructure/postgres/schema.sql index 72eabdf..185fbd3 100644 --- a/server/internal/infrastructure/postgres/schema.sql +++ b/server/internal/infrastructure/postgres/schema.sql @@ -194,7 +194,8 @@ CREATE TABLE IF NOT EXISTS canvas_nodes ( stroke_style TEXT NOT NULL DEFAULT '', flip_x BOOLEAN NOT NULL DEFAULT FALSE, flip_y BOOLEAN NOT NULL DEFAULT FALSE, - rotation DOUBLE PRECISION NOT NULL DEFAULT 0 + rotation DOUBLE PRECISION NOT NULL DEFAULT 0, + semantic_context TEXT NOT NULL DEFAULT '' ); ALTER TABLE canvas_nodes ADD COLUMN IF NOT EXISTS user_id UUID; @@ -225,6 +226,7 @@ ALTER TABLE canvas_nodes ADD COLUMN IF NOT EXISTS stroke_style TEXT NOT NULL DEF ALTER TABLE canvas_nodes ADD COLUMN IF NOT EXISTS flip_x BOOLEAN NOT NULL DEFAULT FALSE; ALTER TABLE canvas_nodes ADD COLUMN IF NOT EXISTS flip_y BOOLEAN NOT NULL DEFAULT FALSE; ALTER TABLE canvas_nodes ADD COLUMN IF NOT EXISTS rotation DOUBLE PRECISION NOT NULL DEFAULT 0; +ALTER TABLE canvas_nodes ADD COLUMN IF NOT EXISTS semantic_context TEXT NOT NULL DEFAULT ''; CREATE INDEX IF NOT EXISTS canvas_nodes_project_id_idx ON canvas_nodes(project_id); CREATE INDEX IF NOT EXISTS canvas_nodes_user_project_idx ON canvas_nodes(user_id, project_id); diff --git a/server/internal/logic/cad_context_mapper_test.go b/server/internal/logic/cad_context_mapper_test.go new file mode 100644 index 0000000..3519210 --- /dev/null +++ b/server/internal/logic/cad_context_mapper_test.go @@ -0,0 +1,22 @@ +package logic + +import ( + "testing" + + "img_infinite_canvas/internal/domain/design" +) + +func TestCanvasNodeSemanticContextRoundTrip(t *testing.T) { + nodes := []design.Node{{ + ID: "cad-1", + Type: design.NodeTypeImage, + Title: "plan.svg", + LayerRole: "cad-vector", + SemanticContext: "CAD semantic context: layer ROOMS", + }} + + roundTrip := toDomainNodes(toAPINodes(nodes)) + if len(roundTrip) != 1 || roundTrip[0].SemanticContext != nodes[0].SemanticContext { + t.Fatalf("expected semantic context round trip, got %#v", roundTrip) + } +} diff --git a/server/internal/logic/mapper.go b/server/internal/logic/mapper.go index 843d34e..711c541 100644 --- a/server/internal/logic/mapper.go +++ b/server/internal/logic/mapper.go @@ -263,6 +263,7 @@ func toAPINodes(items []design.Node) []types.CanvasNode { Status: item.Status, ParentId: item.ParentID, LayerRole: item.LayerRole, + SemanticContext: item.SemanticContext, FontSize: item.FontSize, FontFamily: item.FontFamily, FontWeight: item.FontWeight, @@ -305,6 +306,7 @@ func toDomainNodes(items []types.CanvasNode) []design.Node { Status: item.Status, ParentID: item.ParentId, LayerRole: item.LayerRole, + SemanticContext: item.SemanticContext, FontSize: item.FontSize, FontFamily: item.FontFamily, FontWeight: item.FontWeight, diff --git a/server/internal/types/types.go b/server/internal/types/types.go index 9add9f0..0ccf986 100644 --- a/server/internal/types/types.go +++ b/server/internal/types/types.go @@ -287,6 +287,7 @@ type CanvasNode struct { Status string `json:"status"` ParentId string `json:"parentId,optional"` LayerRole string `json:"layerRole,optional"` + SemanticContext string `json:"semanticContext,optional"` FontSize float64 `json:"fontSize,optional"` FontFamily string `json:"fontFamily,optional"` FontWeight string `json:"fontWeight,optional"`