package world import ( "image/color" "testing" "github.com/stretchr/testify/require" ) func TestRender_AppliesStyleBeforeAddCommands_ForFirstItemInTile(t *testing.T) { t.Parallel() w := NewWorld(10, 10) w.resetGrid(2 * SCALE) // Create a derived circle style so we can observe a style application transition. red := color.RGBA{R: 255, A: 255} styleID := w.AddStyleCircle(StyleOverride{FillColor: red}) _, err := w.AddCircle(5, 5, 1, CircleWithStyleID(styleID), CircleWithPriority(100)) require.NoError(t, err) for _, obj := range w.objects { w.indexObject(obj) } params := RenderParams{ ViewportWidthPx: 10, ViewportHeightPx: 10, MarginXPx: 0, MarginYPx: 0, CameraXWorldFp: 5 * SCALE, CameraYWorldFp: 5 * SCALE, CameraZoom: 1.0, Options: &RenderOptions{ BackgroundColor: color.RGBA{A: 255}, }, } d := &fakePrimitiveDrawer{} require.NoError(t, w.Render(d, params)) cmds := d.Commands() iSetFill := indexOfFirstName(cmds, "SetFillColor") iAddCircle := indexOfFirstName(cmds, "AddCircle") require.NotEqual(t, -1, iSetFill) require.NotEqual(t, -1, iAddCircle) require.Less(t, iSetFill, iAddCircle, "style must be applied before AddCircle") } func TestRender_DoesNotReapplySameStyleAcrossMultipleObjects(t *testing.T) { t.Parallel() w := NewWorld(10, 10) w.resetGrid(2 * SCALE) // Two lines with the same default line style and same priority. _, err := w.AddLine(1, 5, 9, 5, LineWithPriority(100)) require.NoError(t, err) _, err = w.AddLine(1, 6, 9, 6, LineWithPriority(101)) // ensure deterministic order by priority require.NoError(t, err) for _, obj := range w.objects { w.indexObject(obj) } params := RenderParams{ ViewportWidthPx: 10, ViewportHeightPx: 10, MarginXPx: 0, MarginYPx: 0, CameraXWorldFp: 5 * SCALE, CameraYWorldFp: 5 * SCALE, CameraZoom: 1.0, Options: &RenderOptions{ BackgroundColor: color.RGBA{A: 255}, }, } d := &fakePrimitiveDrawer{} require.NoError(t, w.Render(d, params)) // We expect style application at least once. setWidth := d.CommandsByName("SetLineWidth") require.NotEmpty(t, setWidth) // The key batching assertion: style setters should not be called twice *between* two AddLine calls. cmds := d.Commands() line1 := indexOfFirstName(cmds, "AddLine") require.NotEqual(t, -1, line1) line2 := indexOfNextName(cmds, "AddLine", line1+1) require.NotEqual(t, -1, line2) // Between line1 and line2 there must be no SetLineWidth / SetStrokeColor / SetDash / SetDashOffset, // because StyleID is the same and the renderer caches lastStyleID. for i := line1 + 1; i < line2; i++ { switch cmds[i].Name { case "SetLineWidth", "SetStrokeColor", "SetDash", "SetDashOffset", "SetFillColor": t.Fatalf("unexpected style setter %q between two AddLine commands at index %d", cmds[i].Name, i) } } } func TestRender_ReappliesStyleWhenStyleIDChanges(t *testing.T) { t.Parallel() w := NewWorld(10, 10) w.resetGrid(2 * SCALE) // Two circles, different derived fill colors => different StyleIDs. red := color.RGBA{R: 255, A: 255} green := color.RGBA{G: 255, A: 255} styleRed := w.AddStyleCircle(StyleOverride{FillColor: red}) styleGreen := w.AddStyleCircle(StyleOverride{FillColor: green}) _, err := w.AddCircle(4, 5, 1, CircleWithStyleID(styleRed), CircleWithPriority(100)) require.NoError(t, err) _, err = w.AddCircle(6, 5, 1, CircleWithStyleID(styleGreen), CircleWithPriority(101)) require.NoError(t, err) for _, obj := range w.objects { w.indexObject(obj) } params := RenderParams{ ViewportWidthPx: 10, ViewportHeightPx: 10, MarginXPx: 0, MarginYPx: 0, CameraXWorldFp: 5 * SCALE, CameraYWorldFp: 5 * SCALE, CameraZoom: 1.0, Options: &RenderOptions{ BackgroundColor: color.RGBA{A: 255}, }, } d := &fakePrimitiveDrawer{} require.NoError(t, w.Render(d, params)) cmds := d.Commands() firstCircle := indexOfFirstName(cmds, "AddCircle") secondCircle := indexOfNextName(cmds, "AddCircle", firstCircle+1) require.NotEqual(t, -1, firstCircle) require.NotEqual(t, -1, secondCircle) // There must be at least one SetFillColor before each circle. // And importantly, we expect a SetFillColor BETWEEN the two circles due to style change. setBeforeFirst := lastIndexOfNameBefore(cmds, "SetFillColor", firstCircle) require.NotEqual(t, -1, setBeforeFirst) setBetween := indexOfFirstNameInRange(cmds, "SetFillColor", firstCircle+1, secondCircle) require.NotEqual(t, -1, setBetween, "expected style reapply (SetFillColor) between circles with different StyleIDs") } /* ---------- helper functions for fake command slices ---------- */ func indexOfFirstName(cmds []fakeDrawerCommand, name string) int { for i, c := range cmds { if c.Name == name { return i } } return -1 } func indexOfNextName(cmds []fakeDrawerCommand, name string, start int) int { for i := start; i < len(cmds); i++ { if cmds[i].Name == name { return i } } return -1 } func lastIndexOfNameBefore(cmds []fakeDrawerCommand, name string, before int) int { if before > len(cmds) { before = len(cmds) } for i := before - 1; i >= 0; i-- { if cmds[i].Name == name { return i } } return -1 } func indexOfFirstNameInRange(cmds []fakeDrawerCommand, name string, start, end int) int { if start < 0 { start = 0 } if end > len(cmds) { end = len(cmds) } for i := start; i < end; i++ { if cmds[i].Name == name { return i } } return -1 }