Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Copilot Instructions for draw2d

## Project Overview

draw2d is a Go 2D vector graphics library with multiple backends:
- `draw2d` — Core package: interfaces (`GraphicContext`, `PathBuilder`), types (`Matrix`, `Path`), and font management
- `draw2dbase` — Base implementations shared across backends (`StackGraphicContext`, flattener, stroker, dasher)
- `draw2dimg` — Raster image backend (using freetype-go)
- `draw2dpdf` — PDF backend (using gofpdf)
- `draw2dsvg` — SVG backend
- `draw2dgl` — OpenGL backend
- `draw2dkit` — Drawing helpers (`Rectangle`, `Circle`, `Ellipse`, `RoundedRectangle`)
- `samples/` — Example drawings used as integration tests

## Language and Conventions

- All code, comments, commit messages, and documentation must be written in **English**.
- The project uses **Go 1.20+** (see `go.mod`).

## Code Style

### File Headers

Source files include a copyright header and creation date:
```go
// Copyright 2010 The draw2d Authors. All rights reserved.
// created: 21/11/2010 by Laurent Le Goff
```

New files should follow the same pattern with the current date and author name.

### Comments

- Exported types, functions, and methods must have GoDoc comments.
- Comments should start with the name of the thing being documented:
```go
// Rectangle draws a rectangle using a path between (x1,y1) and (x2,y2)
func Rectangle(path draw2d.PathBuilder, x1, y1, x2, y2 float64) {
```
- Package comments go in the main source file or a `doc.go` file.

### Naming

- Follow standard Go naming conventions (camelCase for unexported, PascalCase for exported).
- Backend packages are named `draw2d<backend>` (e.g., `draw2dimg`, `draw2dpdf`).
- The `GraphicContext` struct in each backend embeds `*draw2dbase.StackGraphicContext`.

### Error Handling

- Functions that can fail return `error` as the last return value.
- Do not silently ignore errors — log or return them.

## Testing

### Structure

- **Unit tests** go alongside the source file they test (e.g., `matrix_test.go` tests `matrix.go`).
- **Integration/sample tests** live in `samples_test.go`, `draw2dpdf/samples_test.go`, etc.
- Test output files go in the `output/` directory (generated, not committed).

### Writing Tests

- Use the standard `testing` package only — no external test frameworks.
- Use table-driven tests where multiple inputs share the same logic:
```go
tests := []struct {
name string
// ...
}{
{"case1", ...},
{"case2", ...},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { ... })
}
```
- Tests must not depend on external resources (fonts, network) unless testing that specific integration.
- For image-based tests, use `image.NewRGBA(image.Rect(0, 0, w, h))` as the canvas.
- Use `t.TempDir()` for any file output in tests.
- Reference GitHub issue numbers in regression test comments:
```go
// Test related to issue #95: DashVertexConverter state preservation
```

### Running Tests

```bash
go test ./...
go test -cover ./... | grep -v "no test"
```

### Test Coverage Goals

- Every exported function and method should have at least one unit test.
- Core types (`Matrix`, `Path`, `StackGraphicContext`) should have thorough coverage.
- Backend-specific operations (`Stroke`, `Fill`, `FillStroke`, `Clear`) should verify pixel output where possible.
- Known bugs in the issue tracker should have corresponding regression tests.

## Documentation

- When adding or changing public API, update the GoDoc comments accordingly.
- When fixing a bug, add a comment referencing the issue number.
- If a change affects behavior described in `README.md` or package READMEs, update them.
- The `samples/` directory serves as living documentation — keep samples working after changes.

## Architecture Notes

- All backends implement the `draw2d.GraphicContext` interface defined in `gc.go`.
- `draw2dbase.StackGraphicContext` provides the common state management (colors, transforms, font, path). Backends embed it and override rendering methods (`Stroke`, `Fill`, `FillStroke`, string drawing, etc.).
- The `draw2dkit` helpers operate on `draw2d.PathBuilder`, not `GraphicContext`, making them backend-agnostic.
- `Matrix` is a `[6]float64` affine transformation matrix. Coordinate system follows the HTML Canvas 2D Context conventions.
127 changes: 127 additions & 0 deletions draw2dbase/dasher_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright 2010 The draw2d Authors. All rights reserved.
// created: 07/02/2026 by draw2d contributors

package draw2dbase

import (
"testing"
)

// Test related to issue #95: DashVertexConverter state preservation
func TestDashVertexConverter_StatePreservation(t *testing.T) {
segPath := &SegmentedPath{}
dash := []float64{5, 5}
dasher := NewDashConverter(dash, 0, segPath)

dasher.MoveTo(0, 0)
initialLen := len(segPath.Points)

dasher.LineTo(10, 0)
afterFirstLen := len(segPath.Points)

dasher.LineTo(20, 0)
afterSecondLen := len(segPath.Points)

// Second LineTo should add more points
if afterSecondLen <= afterFirstLen {
t.Error("Second LineTo should add more points, state may not be preserved")
}
if initialLen >= afterFirstLen {
t.Error("First LineTo should add points")
}
}

func TestDashVertexConverter_SingleDash(t *testing.T) {
segPath := &SegmentedPath{}
dash := []float64{10}
dasher := NewDashConverter(dash, 0, segPath)

dasher.MoveTo(0, 0)
dasher.LineTo(50, 0)

// Should produce output
if len(segPath.Points) == 0 {
t.Error("Single-element dash array should produce output")
}
}

func TestDashVertexConverter_DashOffset(t *testing.T) {
segPath1 := &SegmentedPath{}
segPath2 := &SegmentedPath{}
dash := []float64{5, 5}

dasher1 := NewDashConverter(dash, 0, segPath1)
dasher1.MoveTo(0, 0)
dasher1.LineTo(50, 0)

dasher2 := NewDashConverter(dash, 2.5, segPath2)
dasher2.MoveTo(0, 0)
dasher2.LineTo(50, 0)

// Different offsets should produce different output
if len(segPath1.Points) == len(segPath2.Points) {
// Check if points are actually different
allSame := true
minLen := len(segPath1.Points)
if len(segPath2.Points) < minLen {
minLen = len(segPath2.Points)
}
for i := 0; i < minLen; i++ {
if segPath1.Points[i] != segPath2.Points[i] {
allSame = false
break
}
}
if allSame && len(segPath1.Points) > 0 {
t.Error("Different dash offsets should produce different output")
}
}
}

func TestDashVertexConverter_Close(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Errorf("Close panicked: %v", r)
}
}()
segPath := &SegmentedPath{}
dash := []float64{5, 5}
dasher := NewDashConverter(dash, 0, segPath)
dasher.MoveTo(0, 0)
dasher.LineTo(10, 10)
dasher.Close()
}

func TestDashVertexConverter_End(t *testing.T) {
defer func() {
if r := recover(); r != nil {
t.Errorf("End panicked: %v", r)
}
}()
segPath := &SegmentedPath{}
dash := []float64{5, 5}
dasher := NewDashConverter(dash, 0, segPath)
dasher.MoveTo(0, 0)
dasher.LineTo(10, 10)
dasher.End()
}

func TestDashVertexConverter_MoveTo(t *testing.T) {
segPath := &SegmentedPath{}
dash := []float64{5, 5}
dasher := NewDashConverter(dash, 0, segPath)

dasher.MoveTo(10, 20)
// Check that position is set correctly
if dasher.x != 10 || dasher.y != 20 {
t.Errorf("MoveTo should set position to (10, 20), got (%f, %f)", dasher.x, dasher.y)
}
// Check that distance is reset to dashOffset
if dasher.distance != dasher.dashOffset {
t.Error("MoveTo should reset distance to dashOffset")
}
// Check that currentDash is reset
if dasher.currentDash != 0 {
t.Error("MoveTo should reset currentDash to 0")
}
}
134 changes: 134 additions & 0 deletions draw2dbase/flattener_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
// Copyright 2010 The draw2d Authors. All rights reserved.
// created: 07/02/2026 by draw2d contributors

package draw2dbase

import (
"testing"

"github.com/llgcode/draw2d"
)

func TestFlatten_EmptyPath(t *testing.T) {
p := new(draw2d.Path)
segPath := &SegmentedPath{}
Flatten(p, segPath, 1.0)
if len(segPath.Points) != 0 {
t.Error("Empty path should produce no points")
}
}

func TestFlatten_MoveTo(t *testing.T) {
p := new(draw2d.Path)
p.MoveTo(10, 20)
segPath := &SegmentedPath{}
Flatten(p, segPath, 1.0)
if len(segPath.Points) < 2 {
t.Error("MoveTo should add points to segmented path")
}
if segPath.Points[0] != 10 || segPath.Points[1] != 20 {
t.Errorf("MoveTo point = (%f, %f), want (10, 20)", segPath.Points[0], segPath.Points[1])
}
}

func TestFlatten_LineSegments(t *testing.T) {
p := new(draw2d.Path)
p.MoveTo(0, 0)
p.LineTo(10, 10)
segPath := &SegmentedPath{}
Flatten(p, segPath, 1.0)
// Should have at least 4 points (MoveTo + LineTo)
if len(segPath.Points) < 4 {
t.Errorf("MoveTo + LineTo should have at least 4 points, got %d", len(segPath.Points))
}
}

func TestFlatten_WithClose(t *testing.T) {
p := new(draw2d.Path)
p.MoveTo(0, 0)
p.LineTo(10, 0)
p.LineTo(10, 10)
p.Close()
segPath := &SegmentedPath{}
Flatten(p, segPath, 1.0)
// Close should add a line back to start
lastIdx := len(segPath.Points) - 2
if lastIdx >= 0 {
lastX, lastY := segPath.Points[lastIdx], segPath.Points[lastIdx+1]
// Should be back at start (0, 0)
if lastX != 0 || lastY != 0 {
t.Errorf("After Close, last point should be (0, 0), got (%f, %f)", lastX, lastY)
}
}
}

func TestTransformer_Identity(t *testing.T) {
segPath := &SegmentedPath{}
tr := Transformer{
Tr: draw2d.NewIdentityMatrix(),
Flattener: segPath,
}
tr.MoveTo(10, 20)
tr.LineTo(30, 40)
// Identity transform should pass through
if segPath.Points[0] != 10 || segPath.Points[1] != 20 {
t.Error("Identity transform should pass through points")
}
if segPath.Points[2] != 30 || segPath.Points[3] != 40 {
t.Error("Identity transform should pass through points")
}
}

func TestTransformer_Translation(t *testing.T) {
segPath := &SegmentedPath{}
tr := Transformer{
Tr: draw2d.NewTranslationMatrix(5, 10),
Flattener: segPath,
}
tr.MoveTo(10, 20)
// Should be translated to (15, 30)
if segPath.Points[0] != 15 || segPath.Points[1] != 30 {
t.Errorf("Translation transform: point = (%f, %f), want (15, 30)", segPath.Points[0], segPath.Points[1])
}
}

func TestSegmentedPath_MoveTo(t *testing.T) {
segPath := &SegmentedPath{}
segPath.MoveTo(10, 20)
if len(segPath.Points) != 2 {
t.Error("MoveTo should append 2 points")
}
if segPath.Points[0] != 10 || segPath.Points[1] != 20 {
t.Error("MoveTo should append correct coordinates")
}
}

func TestSegmentedPath_LineTo(t *testing.T) {
segPath := &SegmentedPath{}
segPath.MoveTo(0, 0)
segPath.LineTo(10, 10)
if len(segPath.Points) != 4 {
t.Error("MoveTo + LineTo should have 4 points")
}
if segPath.Points[2] != 10 || segPath.Points[3] != 10 {
t.Error("LineTo should append correct coordinates")
}
}

func TestDemuxFlattener(t *testing.T) {
segPath1 := &SegmentedPath{}
segPath2 := &SegmentedPath{}
demux := DemuxFlattener{
Flatteners: []Flattener{segPath1, segPath2},
}
demux.MoveTo(10, 20)
demux.LineTo(30, 40)

// Both flatteners should receive the calls
if len(segPath1.Points) != 4 || len(segPath2.Points) != 4 {
t.Error("DemuxFlattener should dispatch to all flatteners")
}
if segPath1.Points[0] != 10 || segPath2.Points[0] != 10 {
t.Error("DemuxFlattener should dispatch correct values")
}
}
Loading