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 }