Files
root 44406b72db Initial commit: img-infinite-canvas AI design workbench MVP
Moteva-style AI design workbench replica. Home prompt creates a project;
the project page provides an infinite canvas with node drag, zoom/pan,
a design chat panel, history replay, image asset upload, and project
save/regenerate.

- Backend: go-zero, DDD layering, sqlc/pgx, optional PostgreSQL; memory
  or Redis cache; asynq job queue; MinIO/S3/R2/OSS object storage;
  sky-valley/pi agent runtime adapter.
- Frontend: Next.js App Router + Vite artifact build, TypeScript, i18n,
  shadcn/ui components, auth (OTP/Turnstile/Google/WeChat).
- Deploy: Docker Compose and k3s manifests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 23:15:37 +08:00

920 lines
30 KiB
Go

package application
import (
"bytes"
"context"
"crypto/md5"
"encoding/base64"
"encoding/binary"
"encoding/hex"
"fmt"
"image"
"image/color"
"math"
"net/http"
"strings"
"sync"
"time"
"img_infinite_canvas/internal/domain/design"
)
var mockupBlobStore sync.Map
const mockupModelVersion = 4
type mockupBlob struct {
Data []byte
Width int
Height int
}
type mockupModelComputation struct {
Placement design.MockupPlacement
Surface design.MockupSurface
Model design.MockupModel
SourceContent string
}
func (s *DesignService) PrepareMockupSurface(ctx context.Context, projectID string, nodeID string) (design.MockupSurfaceResult, error) {
project, err := s.repo.Get(ctx, strings.TrimSpace(projectID))
if err != nil {
return design.MockupSurfaceResult{}, err
}
nodeIndex := findNodeIndex(project.Nodes, strings.TrimSpace(nodeID))
if nodeIndex < 0 {
return design.MockupSurfaceResult{}, design.ErrNotFound
}
target := project.Nodes[nodeIndex]
if target.Type != design.NodeTypeImage && target.Type != design.NodeTypeFrame {
return design.MockupSurfaceResult{}, fmt.Errorf("%w: mockup surface requires an image node", design.ErrInvalidInput)
}
return mockupSurfaceForNode(target), nil
}
func (s *DesignService) GenerateMockupModel(ctx context.Context, projectID string, nodeID string) (design.MockupModelResult, error) {
project, err := s.repo.Get(ctx, strings.TrimSpace(projectID))
if err != nil {
return design.MockupModelResult{}, err
}
nodeIndex := findNodeIndex(project.Nodes, strings.TrimSpace(nodeID))
if nodeIndex < 0 {
return design.MockupModelResult{}, design.ErrNotFound
}
target := project.Nodes[nodeIndex]
if err := validateMockupModelTarget(target); err != nil {
return design.MockupModelResult{}, err
}
computed, err := s.computeMockupModel(ctx, project, target)
if err != nil {
return design.MockupModelResult{}, err
}
target.MockupModel = computed.Model
target.MockupSurface = computed.Surface
target.MockupSourceContent = computed.SourceContent
project.Nodes[nodeIndex] = target
project = s.refreshCanvasSnapshot(project, s.now())
if err := s.repo.Save(ctx, project); err != nil {
return design.MockupModelResult{}, err
}
return design.MockupModelResult{
Placement: computed.Placement,
Model: computed.Model,
}, nil
}
func (s *DesignService) GenerateMockupModelAsync(ctx context.Context, projectID string, nodeID string) (design.AgentThread, error) {
project, err := s.repo.Get(ctx, strings.TrimSpace(projectID))
if err != nil {
return design.AgentThread{}, err
}
nodeIndex := findNodeIndex(project.Nodes, strings.TrimSpace(nodeID))
if nodeIndex < 0 {
return design.AgentThread{}, design.ErrNotFound
}
target := project.Nodes[nodeIndex]
if err := validateMockupModelTarget(target); err != nil {
return design.AgentThread{}, err
}
now := s.now()
threadID := newID()
status := "running"
messageStatus := "running"
title := "生成 Mockup 模型"
content := "正在分析图片表面、深度、分割和摄像机参数。"
if mockupModelCurrent(target) {
if len(target.MockupSurface.Polygon) < 3 {
target.MockupSurface = mockupSurfaceForNode(target).Surface
project.Nodes[nodeIndex] = target
}
status = "success"
messageStatus = "success"
title = "Mockup 模型已就绪"
content = "已复用当前图片的 Mockup 模型参数。"
} else {
project.Nodes[nodeIndex].Status = "mockup-modeling"
}
project.Status = projectStatusAfterCanvasTask(project.Nodes, design.StatusReady)
project.LastThreadID = threadID
project.UpdatedAt = now
inputArgs := design.GeneratorTaskInputArgs{
AspectRatio: aspectRatioLabel(int64(math.Round(target.Width)), int64(math.Round(target.Height))),
Image: []string{target.Content},
Prompt: "Generate mockup surface model",
Resolution: "1K",
}
project.Messages = append(project.Messages, design.Message{
ID: newID(),
Role: "tool",
Type: "tool",
Title: title,
Content: content,
ThreadID: threadID,
ActionID: threadID,
StepID: newID(),
Name: "mockup_model",
ToolHint: "mockup_model",
Status: messageStatus,
CreatedAt: now,
}, generatorTaskMetadataMessage(threadID, "mockup/model", inputArgs, generatorTaskPrice(inputArgs, "mockup/model"), now.Add(time.Millisecond)))
project = s.refreshCanvasSnapshot(project, s.now())
if err := s.repo.Save(ctx, project); err != nil {
return design.AgentThread{}, err
}
if status == "running" {
s.startMockupModelGeneration(project.ID, project.UserID, threadID, target)
}
return design.AgentThread{
ThreadID: threadID,
Status: status,
ThreadIDType: 9,
Project: project,
}, nil
}
func (s *DesignService) startMockupModelGeneration(projectID string, userID string, threadID string, target design.Node) {
s.enqueueOrRunJob(design.Job{
Kind: design.JobMockupModelGeneration,
UserID: userID,
ProjectID: projectID,
ThreadID: threadID,
Target: target,
}, func(ctx context.Context) error {
if err := s.completeMockupModelGeneration(ctx, projectID, threadID, target); err != nil {
return s.markMockupModelFailed(detachedUserContext(ctx), projectID, threadID, target, err)
}
return nil
})
}
func (s *DesignService) completeMockupModelGeneration(ctx context.Context, projectID string, threadID string, originalTarget design.Node) error {
project, err := s.repo.Get(ctx, projectID)
if err != nil {
return err
}
nodeIndex := findNodeIndex(project.Nodes, originalTarget.ID)
if nodeIndex < 0 {
return design.ErrNotFound
}
target := project.Nodes[nodeIndex]
if err := validateMockupModelTarget(target); err != nil {
return err
}
if strings.TrimSpace(originalTarget.Content) != "" && strings.TrimSpace(target.Content) != strings.TrimSpace(originalTarget.Content) {
return fmt.Errorf("%w: mockup source image changed before model generation completed", design.ErrConflict)
}
computed, err := s.computeMockupModel(ctx, project, target)
if err != nil {
return err
}
project, err = s.repo.Get(ctx, projectID)
if err != nil {
return err
}
nodeIndex = findNodeIndex(project.Nodes, originalTarget.ID)
if nodeIndex < 0 {
return design.ErrNotFound
}
current := project.Nodes[nodeIndex]
if strings.TrimSpace(current.Content) != strings.TrimSpace(computed.SourceContent) {
return fmt.Errorf("%w: mockup source image changed before model generation completed", design.ErrConflict)
}
now := s.now()
current.MockupModel = computed.Model
current.MockupSurface = computed.Surface
current.MockupSourceContent = computed.SourceContent
current.Status = restoredNodeStatus(originalTarget.Status)
project.Nodes[nodeIndex] = current
project.Status = projectStatusAfterCanvasTask(project.Nodes, design.StatusReady)
project.UpdatedAt = now
project.Messages = append(project.Messages, design.Message{
ID: newID(),
Role: "tool",
Type: "tool",
Title: "Mockup 模型完成",
Content: "已生成深度、分割、偏移和摄像机参数,可以继续放置设计图。",
ThreadID: threadID,
ActionID: threadID,
StepID: newID(),
Name: "mockup_model",
ToolHint: "mockup_model",
Status: "success",
CreatedAt: now,
})
project = s.refreshCanvasSnapshot(project, now)
return s.repo.Save(ctx, project)
}
func (s *DesignService) markMockupModelFailed(ctx context.Context, projectID string, threadID string, originalTarget design.Node, failure error) error {
project, err := s.repo.Get(ctx, projectID)
if err != nil {
return err
}
nodeIndex := findNodeIndex(project.Nodes, originalTarget.ID)
if nodeIndex >= 0 {
project.Nodes[nodeIndex].Status = restoredNodeStatus(originalTarget.Status)
}
now := s.now()
project.Status = projectStatusAfterCanvasTask(project.Nodes, design.StatusFailed)
project.UpdatedAt = now
project.Messages = append(project.Messages, design.Message{
ID: newID(),
Role: "error",
Type: "error",
Title: "Mockup 模型生成失败",
Content: failure.Error(),
ThreadID: threadID,
ActionID: threadID,
StepID: newID(),
Name: "mockup_model",
ToolHint: "mockup_model",
Status: "failed",
CreatedAt: now,
})
project = s.refreshCanvasSnapshot(project, now)
return s.repo.Save(ctx, project)
}
func (s *DesignService) computeMockupModel(ctx context.Context, project design.Project, target design.Node) (mockupModelComputation, error) {
if mockupModelCurrent(target) {
surface := target.MockupSurface
if len(surface.Polygon) < 3 {
surface = mockupSurfaceForNode(target).Surface
}
return mockupModelComputation{
Placement: mockupPlacementFromModel(target.MockupModel, target),
Surface: surface,
Model: target.MockupModel,
SourceContent: target.Content,
}, nil
}
if s.mockupAnalyzer == nil {
return mockupModelComputation{}, fmt.Errorf("%w: mockup model analyzer is not configured", ErrUnsupportedAgent)
}
sourceData, contentType, err := loadImageContent(ctx, target.Content)
if err != nil {
return mockupModelComputation{}, err
}
source, _, err := image.Decode(bytes.NewReader(sourceData))
if err != nil {
return mockupModelComputation{}, err
}
imageDataURL := mockupImageDataURL(sourceData, contentType)
analysis, err := s.mockupAnalyzer.AnalyzeMockupModel(ctx, design.MockupModelAnalysisRequest{
Project: project,
Node: target,
ImageDataURL: imageDataURL,
ImageWidth: source.Bounds().Dx(),
ImageHeight: source.Bounds().Dy(),
})
if err != nil {
return mockupModelComputation{}, err
}
placement, surface, model, err := normalizeMockupAnalysis(target, analysis)
if err != nil {
return mockupModelComputation{}, err
}
return mockupModelComputation{
Placement: placement,
Surface: surface,
Model: model,
SourceContent: target.Content,
}, nil
}
func validateMockupModelTarget(target design.Node) error {
if target.Type != design.NodeTypeImage && target.Type != design.NodeTypeFrame {
return fmt.Errorf("%w: mockup model requires an image node", design.ErrInvalidInput)
}
return nil
}
func (s *DesignService) GetMockupBlob(ctx context.Context, dataID string) ([]byte, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
value, ok := mockupBlobStore.Load(strings.TrimSpace(dataID))
if !ok {
repo, repoOK := s.repo.(design.MockupBlobRepository)
if !repoOK {
return nil, design.ErrNotFound
}
blob, err := repo.GetMockupBlob(ctx, strings.TrimSpace(dataID))
if err != nil {
return nil, err
}
storeMockupBlob(blob.DataID, blob.Data, blob.Width, blob.Height)
return append([]byte(nil), blob.Data...), nil
}
blob, ok := value.(mockupBlob)
if !ok {
return nil, design.ErrNotFound
}
return append([]byte(nil), blob.Data...), nil
}
func mockupSurfaceForNode(node design.Node) design.MockupSurfaceResult {
placement := defaultMockupPlacement(node)
polygon := defaultMockupPolygon(node)
return design.MockupSurfaceResult{
Placement: placement,
Surface: design.MockupSurface{
TargetID: node.ID,
Polygon: polygon,
Mesh: mockupMeshFromPolygon(polygon, 5, 5),
},
}
}
func mockupImageDataURL(data []byte, contentType string) string {
contentType = strings.TrimSpace(strings.Split(contentType, ";")[0])
if contentType == "" || !strings.HasPrefix(contentType, "image/") {
contentType = http.DetectContentType(data)
}
if !strings.HasPrefix(contentType, "image/") {
contentType = "image/png"
}
return "data:" + contentType + ";base64," + base64.StdEncoding.EncodeToString(data)
}
func normalizeMockupAnalysis(target design.Node, analysis design.MockupModelAnalysis) (design.MockupPlacement, design.MockupSurface, design.MockupModel, error) {
surface := analysis.Surface
surface.TargetID = target.ID
if len(surface.Polygon) < 3 {
return design.MockupPlacement{}, design.MockupSurface{}, design.MockupModel{}, fmt.Errorf("%w: mockup analyzer returned no printable surface polygon", design.ErrInvalidInput)
}
surface.Polygon = normalizeMockupPoints(surface.Polygon)
if surface.Mesh.Columns < 2 || surface.Mesh.Rows < 2 || len(surface.Mesh.Points) == 0 {
surface.Mesh = mockupMeshFromPolygon(surface.Polygon, 5, 5)
} else {
surface.Mesh.Points = normalizeMockupPoints(surface.Mesh.Points)
}
model := analysis.Model
if model.Version < mockupModelVersion {
model.Version = mockupModelVersion
}
var err error
if model.DepthMap, err = normalizeMockupModelMap(model.DepthMap, "depthMap", 0, 1); err != nil {
return design.MockupPlacement{}, design.MockupSurface{}, design.MockupModel{}, err
}
if model.ShadeMap, err = normalizeMockupModelMap(model.ShadeMap, "shadeMap", 0, 1); err != nil {
return design.MockupPlacement{}, design.MockupSurface{}, design.MockupModel{}, err
}
if model.SegmentationMap, err = normalizeMockupModelMap(model.SegmentationMap, "segmentationMap", 0, 255); err != nil {
return design.MockupPlacement{}, design.MockupSurface{}, design.MockupModel{}, err
}
if model.XOffsetMap, err = normalizeMockupModelMap(model.XOffsetMap, "xOffsetMap", -1, 1); err != nil {
return design.MockupPlacement{}, design.MockupSurface{}, design.MockupModel{}, err
}
if model.YOffsetMap, err = normalizeMockupModelMap(model.YOffsetMap, "yOffsetMap", -1, 1); err != nil {
return design.MockupPlacement{}, design.MockupSurface{}, design.MockupModel{}, err
}
if len(model.CameraIntrinsics.Matrix) != 9 {
return design.MockupPlacement{}, design.MockupSurface{}, design.MockupModel{}, fmt.Errorf("%w: mockup analyzer returned invalid cameraIntrinsics", design.ErrInvalidInput)
}
model.CameraIntrinsics.Matrix = append([]float64(nil), model.CameraIntrinsics.Matrix...)
for index, value := range model.CameraIntrinsics.Matrix {
if math.IsNaN(value) || math.IsInf(value, 0) {
return design.MockupPlacement{}, design.MockupSurface{}, design.MockupModel{}, fmt.Errorf("%w: mockup analyzer returned invalid cameraIntrinsics", design.ErrInvalidInput)
}
if index == 0 || index == 4 {
model.CameraIntrinsics.Matrix[index] = clampMockupFloat(value, 0.2, 6)
continue
}
if index == 2 || index == 5 {
model.CameraIntrinsics.Matrix[index] = clampMockupFloat(value, 0, 1)
}
}
model.DepthScaleMin = clampMockupFloat(model.DepthScaleMin, 0, 1)
if model.DepthScaleMax <= model.DepthScaleMin {
model.DepthScaleMax = 1
}
model.DepthScaleMax = clampMockupFloat(model.DepthScaleMax, model.DepthScaleMin+0.01, 1)
if strings.TrimSpace(model.ColorCorrection) == "" {
model.ColorCorrection = "limited_brightness"
}
placement := analysis.Placement
if placement.Width <= 0 || placement.Height <= 0 {
placement = mockupPlacementFromSurface(surface)
}
placement = normalizeMockupPlacement(placement)
if mockupSurfaceCoversWholeProduct(placement) || mockupSurfaceTooLogoBound(target, placement) {
defaultSurface := mockupSurfaceForNode(target)
placement = defaultSurface.Placement
surface = defaultSurface.Surface
}
if mockupSegmentationSurfaceCoverage(model.SegmentationMap, surface.Polygon) < 0.28 {
model.SegmentationMap = mockupModelMapFromSurface(surface.Polygon, model.SegmentationMap.Width, model.SegmentationMap.Height)
}
return placement, surface, model, nil
}
func mockupSurfaceCoversWholeProduct(placement design.MockupPlacement) bool {
area := placement.Width * placement.Height
return placement.Width > 0.5 || placement.Height > 0.58 || area > 0.28
}
func mockupSurfaceTooLogoBound(target design.Node, placement design.MockupPlacement) bool {
aspect := target.Width / math.Max(target.Height, 1)
area := placement.Width * placement.Height
centered := placement.X > 0.18 && placement.X+placement.Width < 0.82 && placement.Y > 0.16 && placement.Y < 0.56
garmentLikeFrame := aspect > 0.72 && aspect < 1.38
return garmentLikeFrame && centered && placement.Width >= 0.28 && (placement.Height < 0.22 || area < 0.082)
}
func mockupSegmentationSurfaceCoverage(segmentation design.MockupModelMap, polygon []design.MockupPoint) float64 {
if segmentation.Width <= 0 || segmentation.Height <= 0 || len(segmentation.Data) != segmentation.Width*segmentation.Height || len(polygon) < 3 {
return 0
}
inside := 0
covered := 0
for y := 0; y < segmentation.Height; y++ {
v := (float64(y) + 0.5) / float64(segmentation.Height)
for x := 0; x < segmentation.Width; x++ {
u := (float64(x) + 0.5) / float64(segmentation.Width)
if !pointInMockupPolygon(u, v, polygon) {
continue
}
inside++
if segmentation.Data[y*segmentation.Width+x] >= 16 {
covered++
}
}
}
if inside == 0 {
return 0
}
return float64(covered) / float64(inside)
}
func mockupModelMapFromSurface(polygon []design.MockupPoint, width int, height int) design.MockupModelMap {
if width < 2 {
width = 32
}
if height < 2 {
height = 32
}
data := make([]float64, width*height)
for y := 0; y < height; y++ {
v := (float64(y) + 0.5) / float64(height)
for x := 0; x < width; x++ {
u := (float64(x) + 0.5) / float64(width)
data[y*width+x] = mockupSurfaceMaskValue(u, v, polygon, width, height)
}
}
return design.MockupModelMap{Width: width, Height: height, Data: data}
}
func mockupSurfaceMaskValue(u float64, v float64, polygon []design.MockupPoint, width int, height int) float64 {
if len(polygon) < 3 {
return 0
}
inside := 0
for _, offset := range []struct {
x float64
y float64
}{
{-0.35, -0.35},
{0.35, -0.35},
{-0.35, 0.35},
{0.35, 0.35},
} {
su := clampMockupFloat(u+offset.x/float64(width), 0, 1)
sv := clampMockupFloat(v+offset.y/float64(height), 0, 1)
if pointInMockupPolygon(su, sv, polygon) {
inside++
}
}
return math.Round(float64(inside) / 4 * 255)
}
func normalizeMockupModelMap(modelMap design.MockupModelMap, name string, min float64, max float64) (design.MockupModelMap, error) {
if modelMap.Width < 2 || modelMap.Height < 2 || modelMap.Width > 128 || modelMap.Height > 128 {
return design.MockupModelMap{}, fmt.Errorf("%w: mockup analyzer returned invalid %s size", design.ErrInvalidInput, name)
}
expected := modelMap.Width * modelMap.Height
if len(modelMap.Data) == 0 {
return design.MockupModelMap{}, fmt.Errorf("%w: mockup analyzer returned empty %s data", design.ErrInvalidInput, name)
}
sourceData := normalizeMockupModelMapData(modelMap.Data, expected)
data := make([]float64, expected)
for index, value := range sourceData {
if math.IsNaN(value) || math.IsInf(value, 0) {
return design.MockupModelMap{}, fmt.Errorf("%w: mockup analyzer returned invalid %s data", design.ErrInvalidInput, name)
}
data[index] = clampMockupFloat(value, min, max)
}
return design.MockupModelMap{Width: modelMap.Width, Height: modelMap.Height, Data: data}, nil
}
func normalizeMockupModelMapData(source []float64, expected int) []float64 {
if len(source) == expected {
return append([]float64(nil), source...)
}
if expected <= 0 || len(source) == 0 {
return nil
}
data := make([]float64, expected)
tolerance := maxInt(4, expected/64)
if absInt(len(source)-expected) <= tolerance {
copy(data, source)
if len(source) < expected {
fill := source[len(source)-1]
for index := len(source); index < expected; index++ {
data[index] = fill
}
}
return data
}
if len(source) == 1 {
for index := range data {
data[index] = source[0]
}
return data
}
if expected == 1 {
data[0] = source[0]
return data
}
scale := float64(len(source)-1) / float64(expected-1)
for index := range data {
position := float64(index) * scale
left := int(math.Floor(position))
right := minInt(left+1, len(source)-1)
weight := position - float64(left)
data[index] = source[left]*(1-weight) + source[right]*weight
}
return data
}
func normalizeMockupPoints(points []design.MockupPoint) []design.MockupPoint {
next := make([]design.MockupPoint, 0, len(points))
for _, point := range points {
next = append(next, design.MockupPoint{X: clampMockupCoord(point.X), Y: clampMockupCoord(point.Y)})
}
return next
}
func normalizeMockupPlacement(placement design.MockupPlacement) design.MockupPlacement {
placement.X = clampMockupFloat(placement.X, 0, 0.98)
placement.Y = clampMockupFloat(placement.Y, 0, 0.98)
placement.Width = clampMockupFloat(placement.Width, 0.02, 1-placement.X)
placement.Height = clampMockupFloat(placement.Height, 0.02, 1-placement.Y)
return placement
}
func mockupPlacementFromSurface(surface design.MockupSurface) design.MockupPlacement {
if len(surface.Polygon) == 0 {
return design.MockupPlacement{X: 0.38, Y: 0.3, Width: 0.24, Height: 0.28}
}
minX, minY := 1.0, 1.0
maxX, maxY := 0.0, 0.0
for _, point := range surface.Polygon {
minX = math.Min(minX, point.X)
minY = math.Min(minY, point.Y)
maxX = math.Max(maxX, point.X)
maxY = math.Max(maxY, point.Y)
}
return normalizeMockupPlacement(design.MockupPlacement{X: minX, Y: minY, Width: maxX - minX, Height: maxY - minY})
}
func mockupPlacementFromModel(model design.MockupModel, target design.Node) design.MockupPlacement {
if len(model.SegmentationMap.Data) == model.SegmentationMap.Width*model.SegmentationMap.Height && model.SegmentationMap.Width > 0 && model.SegmentationMap.Height > 0 {
width := model.SegmentationMap.Width
height := model.SegmentationMap.Height
minX, minY := width, height
maxX, maxY := -1, -1
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
if model.SegmentationMap.Data[y*width+x] < 16 {
continue
}
minX = minInt(minX, x)
minY = minInt(minY, y)
maxX = maxInt(maxX, x)
maxY = maxInt(maxY, y)
}
}
if maxX >= minX && maxY >= minY {
return normalizeMockupPlacement(design.MockupPlacement{
X: float64(minX) / float64(width),
Y: float64(minY) / float64(height),
Width: float64(maxX-minX+1) / float64(width),
Height: float64(maxY-minY+1) / float64(height),
})
}
}
return defaultMockupPlacement(target)
}
func (s *DesignService) registerMockupBlob(ctx context.Context, data []byte, width int, height int) (design.MockupModelMap, error) {
sum := md5.Sum(data)
dataID := hex.EncodeToString(sum[:]) + ".bin"
storeMockupBlob(dataID, data, width, height)
if repo, ok := s.repo.(design.MockupBlobRepository); ok {
if err := repo.SaveMockupBlob(ctx, design.MockupBlobData{
DataID: dataID,
Width: width,
Height: height,
Data: append([]byte(nil), data...),
CreatedAt: time.Now(),
}); err != nil {
return design.MockupModelMap{}, err
}
}
return design.MockupModelMap{
DataID: dataID,
Width: width,
Height: height,
URL: "/api/mockup/blob/" + dataID,
}, nil
}
func storeMockupBlob(dataID string, data []byte, width int, height int) {
mockupBlobStore.Store(dataID, mockupBlob{
Data: append([]byte(nil), data...),
Width: width,
Height: height,
})
}
func hasMockupModel(model design.MockupModel) bool {
return model.Version > 0 &&
hasMockupModelMap(model.DepthMap) &&
hasMockupModelMap(model.ShadeMap) &&
hasMockupModelMap(model.SegmentationMap) &&
hasMockupModelMap(model.XOffsetMap) &&
hasMockupModelMap(model.YOffsetMap)
}
func mockupModelCurrent(target design.Node) bool {
return hasMockupModel(target.MockupModel) &&
target.MockupModel.Version >= mockupModelVersion &&
hasInlineMockupModelData(target.MockupModel) &&
strings.TrimSpace(target.MockupSourceContent) == strings.TrimSpace(target.Content)
}
func hasMockupModelMap(modelMap design.MockupModelMap) bool {
return strings.TrimSpace(modelMap.DataID) != "" || len(modelMap.Data) == modelMap.Width*modelMap.Height && modelMap.Width > 0 && modelMap.Height > 0
}
func hasInlineMockupModelData(model design.MockupModel) bool {
return len(model.DepthMap.Data) == model.DepthMap.Width*model.DepthMap.Height &&
len(model.ShadeMap.Data) == model.ShadeMap.Width*model.ShadeMap.Height &&
len(model.SegmentationMap.Data) == model.SegmentationMap.Width*model.SegmentationMap.Height &&
len(model.XOffsetMap.Data) == model.XOffsetMap.Width*model.XOffsetMap.Height &&
len(model.YOffsetMap.Data) == model.YOffsetMap.Width*model.YOffsetMap.Height
}
func (s *DesignService) hydrateMockupModelBlobs(ctx context.Context, model design.MockupModel) error {
maps := []design.MockupModelMap{
model.DepthMap,
model.ShadeMap,
model.SegmentationMap,
model.XOffsetMap,
model.YOffsetMap,
}
for _, modelMap := range maps {
dataID := strings.TrimSpace(modelMap.DataID)
if dataID == "" {
return design.ErrNotFound
}
if _, ok := mockupBlobStore.Load(dataID); ok {
continue
}
repo, repoOK := s.repo.(design.MockupBlobRepository)
if !repoOK {
return design.ErrNotFound
}
blob, err := repo.GetMockupBlob(ctx, dataID)
if err != nil {
return err
}
storeMockupBlob(blob.DataID, blob.Data, blob.Width, blob.Height)
}
return nil
}
func mockupFloatMap(source image.Image, width int, height int, value func(luma float64, gradientX float64, gradientY float64, u float64, v float64, mask float64) float32) []byte {
output := make([]byte, width*height*4)
for y := 0; y < height; y++ {
v := float64(y) / math.Max(float64(height-1), 1)
for x := 0; x < width; x++ {
u := float64(x) / math.Max(float64(width-1), 1)
luma := mockupImageLuma(source, u, v)
left := mockupImageLuma(source, clampMockupFloat(u-1/float64(width), 0, 1), v)
right := mockupImageLuma(source, clampMockupFloat(u+1/float64(width), 0, 1), v)
top := mockupImageLuma(source, u, clampMockupFloat(v-1/float64(height), 0, 1))
bottom := mockupImageLuma(source, u, clampMockupFloat(v+1/float64(height), 0, 1))
mask := defaultMockupMask(u, v, source.Bounds().Dy() > source.Bounds().Dx())
binary.LittleEndian.PutUint32(output[(y*width+x)*4:], math.Float32bits(value(luma, right-left, bottom-top, u, v, mask)))
}
}
return output
}
func mockupSegmentationMap(target design.Node, width int, height int) []byte {
output := make([]byte, width*height)
polygon := defaultMockupPolygon(target)
for y := 0; y < height; y++ {
v := float64(y) / math.Max(float64(height-1), 1)
for x := 0; x < width; x++ {
u := float64(x) / math.Max(float64(width-1), 1)
var inside int
for _, offset := range []struct {
x float64
y float64
}{
{-0.25, -0.25},
{0.25, -0.25},
{-0.25, 0.25},
{0.25, 0.25},
} {
su := clampMockupFloat(u+offset.x/float64(width), 0, 1)
sv := clampMockupFloat(v+offset.y/float64(height), 0, 1)
if pointInMockupPolygon(su, sv, polygon) {
inside++
}
}
output[y*width+x] = byte(math.Round(float64(inside) / 4 * 255))
}
}
return output
}
func mockupImageLuma(source image.Image, u float64, v float64) float64 {
bounds := source.Bounds()
x := bounds.Min.X + int(clampMockupFloat(u, 0, 1)*float64(bounds.Dx()-1))
y := bounds.Min.Y + int(clampMockupFloat(v, 0, 1)*float64(bounds.Dy()-1))
rgba := color.NRGBAModel.Convert(source.At(x, y)).(color.NRGBA)
return (float64(rgba.R)*0.299 + float64(rgba.G)*0.587 + float64(rgba.B)*0.114) / 255
}
func defaultMockupMask(u float64, v float64, portrait bool) float64 {
if portrait {
if u < 0.12 || u > 0.74 || v < 0.27 || v > 0.84 {
return 0
}
return 1
}
if u < 0.14 || u > 0.86 || v < 0.18 || v > 0.82 {
return 0
}
return 1
}
func defaultMockupPlacement(node design.Node) design.MockupPlacement {
portrait := node.Height > node.Width*1.08
if portrait {
return design.MockupPlacement{X: 0.3, Y: 0.34, Width: 0.4, Height: 0.3}
}
return design.MockupPlacement{X: 0.29, Y: 0.31, Width: 0.42, Height: 0.3}
}
func defaultMockupPolygon(node design.Node) []design.MockupPoint {
placement := defaultMockupPlacement(node)
topInset := placement.Width * 0.045
bottomInset := placement.Width * 0.015
return []design.MockupPoint{
{X: placement.X + topInset, Y: placement.Y},
{X: placement.X + placement.Width - topInset, Y: placement.Y},
{X: placement.X + placement.Width - bottomInset, Y: placement.Y + placement.Height},
{X: placement.X + bottomInset, Y: placement.Y + placement.Height},
}
}
func mockupMeshFromPolygon(polygon []design.MockupPoint, columns int, rows int) design.MockupMesh {
if len(polygon) < 4 {
return design.MockupMesh{}
}
if columns < 2 {
columns = 2
}
if rows < 2 {
rows = 2
}
points := make([]design.MockupPoint, 0, columns*rows)
for y := 0; y < rows; y++ {
v := float64(y) / float64(rows-1)
for x := 0; x < columns; x++ {
u := float64(x) / float64(columns-1)
point := bilinearMockupPoint(polygon[0], polygon[1], polygon[2], polygon[3], u, v)
point.X += math.Sin(v*math.Pi) * math.Sin((u-0.5)*math.Pi) * 0.006
point.Y += math.Sin(u*math.Pi) * math.Sin((v-0.5)*math.Pi) * 0.004
points = append(points, design.MockupPoint{
X: clampMockupCoord(point.X),
Y: clampMockupCoord(point.Y),
})
}
}
return design.MockupMesh{
Columns: columns,
Rows: rows,
Points: points,
}
}
func bilinearMockupPoint(topLeft design.MockupPoint, topRight design.MockupPoint, bottomRight design.MockupPoint, bottomLeft design.MockupPoint, u float64, v float64) design.MockupPoint {
top := lerpMockupPoint(topLeft, topRight, u)
bottom := lerpMockupPoint(bottomLeft, bottomRight, u)
return lerpMockupPoint(top, bottom, v)
}
func lerpMockupPoint(a design.MockupPoint, b design.MockupPoint, t float64) design.MockupPoint {
return design.MockupPoint{
X: a.X + (b.X-a.X)*t,
Y: a.Y + (b.Y-a.Y)*t,
}
}
func pointInMockupPolygon(x float64, y float64, polygon []design.MockupPoint) bool {
if len(polygon) < 3 {
return false
}
inside := false
j := len(polygon) - 1
for i := range polygon {
pi := polygon[i]
pj := polygon[j]
if (pi.Y > y) != (pj.Y > y) {
crossX := (pj.X-pi.X)*(y-pi.Y)/(pj.Y-pi.Y) + pi.X
if x < crossX {
inside = !inside
}
}
j = i
}
return inside
}
func clampMockupFloat(value float64, min float64, max float64) float64 {
if value < min {
return min
}
if value > max {
return max
}
return value
}
func clampMockupCoord(value float64) float64 {
return math.Max(0, math.Min(1, value))
}
func minInt(a int, b int) int {
if a < b {
return a
}
return b
}
func absInt(value int) int {
if value < 0 {
return -value
}
return value
}
func maxInt(a int, b int) int {
if a > b {
return a
}
return b
}