package application import ( "context" "image/color" "testing" "time" "img_infinite_canvas/internal/domain/design" "img_infinite_canvas/internal/infrastructure/memory" ) func TestGenerateMockupModelUsesAnalyzerAndPersistsInlineModelData(t *testing.T) { ctx := context.Background() repo := memory.NewProjectRepository() service := NewDesignService(repo, nil, nil, nil, nil) analyzer := &fakeMockupAnalyzer{} service.SetMockupModelAnalyzer(analyzer) project := design.Project{ ID: "project-mockup-model", Title: "Mockup model", Status: design.StatusReady, UpdatedAt: time.Now(), Nodes: []design.Node{ { ID: "target", Type: design.NodeTypeImage, Title: "Target", Width: 640, Height: 800, Content: testPNGDataURL(color.NRGBA{R: 220, G: 220, B: 210, A: 255}, 6, 8), Status: "success", }, }, } if err := repo.Save(ctx, project); err != nil { t.Fatal(err) } first, err := service.GenerateMockupModel(ctx, project.ID, "target") if err != nil { t.Fatal(err) } if !hasMockupModel(first.Model) { t.Fatalf("expected generated mockup model, got %#v", first.Model) } if len(first.Model.DepthMap.Data) != first.Model.DepthMap.Width*first.Model.DepthMap.Height { t.Fatalf("expected inline depth data, got %#v", first.Model.DepthMap) } saved, err := repo.Get(ctx, project.ID) if err != nil { t.Fatal(err) } if !hasMockupModel(saved.Nodes[0].MockupModel) { t.Fatalf("expected model to be persisted on target node, got %#v", saved.Nodes[0].MockupModel) } if saved.Nodes[0].MockupSourceContent != project.Nodes[0].Content { t.Fatal("expected source content fingerprint to be persisted") } second, err := service.GenerateMockupModel(ctx, project.ID, "target") if err != nil { t.Fatal(err) } if analyzer.calls != 1 { t.Fatalf("expected cached inline model to avoid a second analyzer call, got %d", analyzer.calls) } if len(second.Model.DepthMap.Data) != len(first.Model.DepthMap.Data) { t.Fatal("expected cached inline model data") } } func TestGenerateMockupModelAsyncQueuesAndCompletesInlineModel(t *testing.T) { ctx := context.Background() repo := memory.NewProjectRepository() queue := &fakeJobQueue{} service := NewDesignService(repo, nil, nil, nil, nil) service.SetJobQueue(queue) service.SetMockupModelAnalyzer(&fakeMockupAnalyzer{}) project := design.Project{ ID: "project-mockup-model-async", Title: "Mockup model async", Status: design.StatusReady, UpdatedAt: time.Now(), Nodes: []design.Node{ { ID: "target", Type: design.NodeTypeImage, Title: "Target", Width: 640, Height: 800, Content: testPNGDataURL(color.NRGBA{R: 220, G: 220, B: 210, A: 255}, 6, 8), Status: "success", }, }, } if err := repo.Save(ctx, project); err != nil { t.Fatal(err) } thread, err := service.GenerateMockupModelAsync(ctx, project.ID, "target") if err != nil { t.Fatal(err) } if thread.Status != "running" || thread.ThreadID == "" { t.Fatalf("expected running mockup thread, got %#v", thread) } if len(queue.jobs) != 1 || queue.jobs[0].Kind != design.JobMockupModelGeneration || queue.jobs[0].ThreadID != thread.ThreadID { t.Fatalf("expected mockup model job, got %#v", queue.jobs) } saved, err := repo.Get(ctx, project.ID) if err != nil { t.Fatal(err) } if saved.Nodes[0].Status != "mockup-modeling" || saved.Status != design.StatusExploring { t.Fatalf("expected mockup node to be marked active, got project=%s node=%#v", saved.Status, saved.Nodes[0]) } if len(saved.Messages) != 2 || saved.Messages[0].Name != "mockup_model" || saved.Messages[0].Status != "running" || saved.Messages[1].Name != "generator_task" { t.Fatalf("expected running mockup message, got %#v", saved.Messages) } if err := service.ProcessJob(ctx, queue.jobs[0]); err != nil { t.Fatal(err) } saved, err = repo.Get(ctx, project.ID) if err != nil { t.Fatal(err) } if saved.Nodes[0].Status != "success" || !mockupModelCurrent(saved.Nodes[0]) { t.Fatalf("expected completed inline mockup model, got %#v", saved.Nodes[0]) } if len(saved.Messages) != 3 || saved.Messages[2].Name != "mockup_model" || saved.Messages[2].Status != "success" { t.Fatalf("expected successful mockup message, got %#v", saved.Messages) } } func TestNormalizeMockupModelMapRepairsNearMissDataLength(t *testing.T) { shortData := make([]float64, 254) for index := range shortData { shortData[index] = 0.42 } normalized, err := normalizeMockupModelMap(design.MockupModelMap{Width: 16, Height: 16, Data: shortData}, "depthMap", 0, 1) if err != nil { t.Fatal(err) } if len(normalized.Data) != 256 { t.Fatalf("expected repaired 16x16 data, got %d", len(normalized.Data)) } if normalized.Data[254] != 0.42 || normalized.Data[255] != 0.42 { t.Fatalf("expected missing tail values to be filled from edge value, got tail %#v", normalized.Data[252:]) } longData := make([]float64, 260) for index := range longData { longData[index] = float64(index) } normalized, err = normalizeMockupModelMap(design.MockupModelMap{Width: 16, Height: 16, Data: longData}, "depthMap", 0, 255) if err != nil { t.Fatal(err) } if len(normalized.Data) != 256 || normalized.Data[255] != 255 { t.Fatalf("expected long data to be cropped and clamped, got len=%d last=%v", len(normalized.Data), normalized.Data[255]) } } func TestNormalizeMockupAnalysisRepairsLogoBoundSurfaceAndMismatchedSegmentation(t *testing.T) { target := design.Node{ID: "target", Type: design.NodeTypeImage, Width: 800, Height: 800} analysis := fakeMockupAnalysis() analysis.Placement = design.MockupPlacement{X: 0.32, Y: 0.28, Width: 0.36, Height: 0.16} analysis.Surface.Polygon = []design.MockupPoint{{X: 0.32, Y: 0.28}, {X: 0.68, Y: 0.28}, {X: 0.68, Y: 0.44}, {X: 0.32, Y: 0.44}} analysis.Model.SegmentationMap = mockupTestMap(32, 32, bottomOnlyMockupMask(32, 32)) analysis.Model.CameraIntrinsics.Matrix = []float64{1.4, 0, 333.5, 0, 1.4, 333.5, 0, 0, 1} placement, surface, model, err := normalizeMockupAnalysis(target, analysis) if err != nil { t.Fatal(err) } if placement.Height < 0.29 { t.Fatalf("expected logo-bound surface to expand to the usable print zone, got %#v", placement) } if coverage := mockupSegmentationSurfaceCoverage(model.SegmentationMap, surface.Polygon); coverage < 0.7 { t.Fatalf("expected segmentation to be repaired against printable surface, got coverage %.2f", coverage) } if got := model.CameraIntrinsics.Matrix[2]; got < 0 || got > 1 { t.Fatalf("expected normalized cx, got %v", got) } } type fakeMockupAnalyzer struct { calls int } func (f *fakeMockupAnalyzer) AnalyzeMockupModel(ctx context.Context, req design.MockupModelAnalysisRequest) (design.MockupModelAnalysis, error) { f.calls++ return fakeMockupAnalysis(), nil } func fakeMockupAnalysis() design.MockupModelAnalysis { return design.MockupModelAnalysis{ Placement: design.MockupPlacement{X: 0.2, Y: 0.25, Width: 0.4, Height: 0.5}, Surface: design.MockupSurface{ Polygon: []design.MockupPoint{{X: 0.2, Y: 0.25}, {X: 0.6, Y: 0.25}, {X: 0.6, Y: 0.75}, {X: 0.2, Y: 0.75}}, }, Model: design.MockupModel{ Version: 2, DepthMap: mockupTestMap(2, 2, []float64{0.45, 0.5, 0.52, 0.58}), DepthScaleMin: 0, DepthScaleMax: 1, ShadeMap: mockupTestMap(2, 2, []float64{0.8, 0.82, 0.78, 0.8}), SegmentationMap: mockupTestMap(2, 2, []float64{0, 255, 255, 0}), XOffsetMap: mockupTestMap(2, 2, []float64{-0.1, 0.1, -0.08, 0.08}), YOffsetMap: mockupTestMap(2, 2, []float64{0.02, 0.04, -0.02, -0.04}), CameraIntrinsics: design.MockupCameraIntrinsics{ Matrix: []float64{1.4, 0, 0.5, 0, 1.4, 0.5, 0, 0, 1}, }, ColorCorrection: "limited_brightness", }, } } func mockupTestMap(width int, height int, data []float64) design.MockupModelMap { return design.MockupModelMap{Width: width, Height: height, Data: data} } func bottomOnlyMockupMask(width int, height int) []float64 { data := make([]float64, width*height) for y := height * 3 / 4; y < height; y++ { for x := 0; x < width; x++ { data[y*width+x] = 255 } } return data }