- Core library with SVG and PNG generation - CLI tool with generate and batch commands - Cross-platform path handling for Windows compatibility - Comprehensive test suite with integration tests
311 lines
9.0 KiB
Go
311 lines
9.0 KiB
Go
package engine
|
|
|
|
import "math"
|
|
|
|
// Shape rendering constants for visual proportions
|
|
const (
|
|
// Center shape proportions - these ratios determine the visual appearance
|
|
centerShapeAsymmetricCornerRatio = 0.42 // Shape 0: corner cut proportion
|
|
centerShapeTriangleWidthRatio = 0.5 // Shape 1: triangle width relative to cell
|
|
centerShapeTriangleHeightRatio = 0.8 // Shape 1: triangle height relative to cell
|
|
|
|
centerShapeInnerMarginRatio = 0.1 // Shape 3,5,9,10: inner margin ratio
|
|
centerShapeOuterMarginRatio = 0.25 // Shape 3: outer margin ratio for large cells
|
|
centerShapeOuterMarginRatio9 = 0.35 // Shape 9: outer margin ratio for large cells
|
|
centerShapeOuterMarginRatio10 = 0.12 // Shape 10: inner ratio for circular cutout
|
|
|
|
centerShapeCircleMarginRatio = 0.15 // Shape 4: circle margin ratio
|
|
centerShapeCircleWidthRatio = 0.5 // Shape 4: circle width ratio
|
|
|
|
// Shape 6 complex polygon proportions
|
|
centerShapeComplexHeight1Ratio = 0.7 // First height point
|
|
centerShapeComplexPoint1XRatio = 0.4 // First point X ratio
|
|
centerShapeComplexPoint1YRatio = 0.4 // First point Y ratio
|
|
centerShapeComplexPoint2XRatio = 0.7 // Second point X ratio
|
|
|
|
// Shape 9 rectangular cutout proportions
|
|
centerShapeRect9InnerRatio = 0.14 // Shape 9: inner rectangle ratio
|
|
|
|
// Shape 12 rhombus cutout proportion
|
|
centerShapeRhombusCutoutRatio = 0.25 // Shape 12: rhombus cutout margin
|
|
|
|
// Shape 13 large circle proportions (only for center position)
|
|
centerShapeLargeCircleMarginRatio = 0.4 // Shape 13: circle margin ratio
|
|
centerShapeLargeCircleWidthRatio = 1.2 // Shape 13: circle width ratio
|
|
|
|
// Outer shape proportions
|
|
outerShapeCircleMarginRatio = 1.0 / 6.0 // Shape 3: circle margin (1/6 of cell)
|
|
|
|
// Size thresholds for conditional rendering
|
|
smallCellThreshold4 = 4 // Threshold for shape 3,9 outer margin calculation
|
|
smallCellThreshold6 = 6 // Threshold for shape 3 outer margin calculation
|
|
smallCellThreshold8 = 8 // Threshold for shape 3,9 inner margin floor calculation
|
|
|
|
// Multipliers for margin calculations
|
|
innerOuterMultiplier5 = 4 // Shape 5: inner to outer multiplier
|
|
innerOuterMultiplier10 = 3 // Shape 10: inner to outer multiplier
|
|
)
|
|
|
|
// Point represents a 2D point
|
|
type Point struct {
|
|
X, Y float64
|
|
}
|
|
|
|
// Renderer interface defines the methods that a renderer must implement
|
|
type Renderer interface {
|
|
AddPolygon(points []Point)
|
|
AddCircle(topLeft Point, size float64, invert bool)
|
|
}
|
|
|
|
// Graphics provides helper functions for rendering common basic shapes
|
|
type Graphics struct {
|
|
renderer Renderer
|
|
currentTransform Transform
|
|
}
|
|
|
|
// NewGraphics creates a new Graphics instance with the given renderer
|
|
func NewGraphics(renderer Renderer) *Graphics {
|
|
return &Graphics{
|
|
renderer: renderer,
|
|
currentTransform: NoTransform,
|
|
}
|
|
}
|
|
|
|
// NewGraphicsWithTransform creates a new Graphics instance with the given renderer and transform
|
|
func NewGraphicsWithTransform(renderer Renderer, transform Transform) *Graphics {
|
|
return &Graphics{
|
|
renderer: renderer,
|
|
currentTransform: transform,
|
|
}
|
|
}
|
|
|
|
// AddPolygon adds a polygon to the underlying renderer
|
|
func (g *Graphics) AddPolygon(points []Point, invert bool) {
|
|
// Transform all points
|
|
transformedPoints := make([]Point, len(points))
|
|
if invert {
|
|
// Reverse the order and transform
|
|
for i, p := range points {
|
|
transformedPoints[len(points)-1-i] = g.currentTransform.TransformIconPoint(p.X, p.Y, 0, 0)
|
|
}
|
|
} else {
|
|
// Transform in order
|
|
for i, p := range points {
|
|
transformedPoints[i] = g.currentTransform.TransformIconPoint(p.X, p.Y, 0, 0)
|
|
}
|
|
}
|
|
g.renderer.AddPolygon(transformedPoints)
|
|
}
|
|
|
|
// AddCircle adds a circle to the underlying renderer
|
|
func (g *Graphics) AddCircle(x, y, size float64, invert bool) {
|
|
// Transform the circle position
|
|
transformedPoint := g.currentTransform.TransformIconPoint(x, y, size, size)
|
|
g.renderer.AddCircle(transformedPoint, size, invert)
|
|
}
|
|
|
|
// AddRectangle adds a rectangle to the underlying renderer
|
|
func (g *Graphics) AddRectangle(x, y, w, h float64, invert bool) {
|
|
points := []Point{
|
|
{X: x, Y: y},
|
|
{X: x + w, Y: y},
|
|
{X: x + w, Y: y + h},
|
|
{X: x, Y: y + h},
|
|
}
|
|
g.AddPolygon(points, invert)
|
|
}
|
|
|
|
// AddTriangle adds a right triangle to the underlying renderer
|
|
// r is the rotation (0-3), where 0 = right corner in lower left
|
|
func (g *Graphics) AddTriangle(x, y, w, h float64, r int, invert bool) {
|
|
points := []Point{
|
|
{X: x + w, Y: y},
|
|
{X: x + w, Y: y + h},
|
|
{X: x, Y: y + h},
|
|
{X: x, Y: y},
|
|
}
|
|
|
|
// Remove one corner based on rotation
|
|
removeIndex := (r % 4) * 1
|
|
if removeIndex < len(points) {
|
|
points = append(points[:removeIndex], points[removeIndex+1:]...)
|
|
}
|
|
|
|
g.AddPolygon(points, invert)
|
|
}
|
|
|
|
// AddRhombus adds a rhombus (diamond) to the underlying renderer
|
|
func (g *Graphics) AddRhombus(x, y, w, h float64, invert bool) {
|
|
points := []Point{
|
|
{X: x + w/2, Y: y},
|
|
{X: x + w, Y: y + h/2},
|
|
{X: x + w/2, Y: y + h},
|
|
{X: x, Y: y + h/2},
|
|
}
|
|
g.AddPolygon(points, invert)
|
|
}
|
|
|
|
// RenderCenterShape renders one of the 14 distinct center shape patterns
|
|
func RenderCenterShape(g *Graphics, shapeIndex int, cell, positionIndex float64) {
|
|
index := shapeIndex % 14
|
|
|
|
switch index {
|
|
case 0:
|
|
// Shape 0: Asymmetric polygon
|
|
k := cell * centerShapeAsymmetricCornerRatio
|
|
points := []Point{
|
|
{X: 0, Y: 0},
|
|
{X: cell, Y: 0},
|
|
{X: cell, Y: cell - k*2},
|
|
{X: cell - k, Y: cell},
|
|
{X: 0, Y: cell},
|
|
}
|
|
g.AddPolygon(points, false)
|
|
|
|
case 1:
|
|
// Shape 1: Triangle
|
|
w := math.Floor(cell * centerShapeTriangleWidthRatio)
|
|
h := math.Floor(cell * centerShapeTriangleHeightRatio)
|
|
g.AddTriangle(cell-w, 0, w, h, 2, false)
|
|
|
|
case 2:
|
|
// Shape 2: Rectangle
|
|
w := math.Floor(cell / 3)
|
|
g.AddRectangle(w, w, cell-w, cell-w, false)
|
|
|
|
case 3:
|
|
// Shape 3: Nested rectangles
|
|
inner := cell * centerShapeInnerMarginRatio
|
|
var outer float64
|
|
if cell < smallCellThreshold6 {
|
|
outer = 1
|
|
} else if cell < smallCellThreshold8 {
|
|
outer = 2
|
|
} else {
|
|
outer = math.Floor(cell * centerShapeOuterMarginRatio)
|
|
}
|
|
|
|
if inner > 1 {
|
|
inner = math.Floor(inner)
|
|
} else if inner > 0.5 {
|
|
inner = 1
|
|
}
|
|
|
|
g.AddRectangle(outer, outer, cell-inner-outer, cell-inner-outer, false)
|
|
|
|
case 4:
|
|
// Shape 4: Circle
|
|
m := math.Floor(cell * centerShapeCircleMarginRatio)
|
|
w := math.Floor(cell * centerShapeCircleWidthRatio)
|
|
g.AddCircle(cell-w-m, cell-w-m, w, false)
|
|
|
|
case 5:
|
|
// Shape 5: Rectangle with triangular cutout
|
|
inner := cell * centerShapeInnerMarginRatio
|
|
outer := inner * innerOuterMultiplier5
|
|
|
|
if outer > 3 {
|
|
outer = math.Floor(outer)
|
|
}
|
|
|
|
g.AddRectangle(0, 0, cell, cell, false)
|
|
points := []Point{
|
|
{X: outer, Y: outer},
|
|
{X: cell - inner, Y: outer},
|
|
{X: outer + (cell-outer-inner)/2, Y: cell - inner},
|
|
}
|
|
g.AddPolygon(points, true)
|
|
|
|
case 6:
|
|
// Shape 6: Complex polygon
|
|
points := []Point{
|
|
{X: 0, Y: 0},
|
|
{X: cell, Y: 0},
|
|
{X: cell, Y: cell * centerShapeComplexHeight1Ratio},
|
|
{X: cell * centerShapeComplexPoint1XRatio, Y: cell * centerShapeComplexPoint1YRatio},
|
|
{X: cell * centerShapeComplexPoint2XRatio, Y: cell},
|
|
{X: 0, Y: cell},
|
|
}
|
|
g.AddPolygon(points, false)
|
|
|
|
case 7:
|
|
// Shape 7: Small triangle
|
|
g.AddTriangle(cell/2, cell/2, cell/2, cell/2, 3, false)
|
|
|
|
case 8:
|
|
// Shape 8: Composite shape
|
|
g.AddRectangle(0, 0, cell, cell/2, false)
|
|
g.AddRectangle(0, cell/2, cell/2, cell/2, false)
|
|
g.AddTriangle(cell/2, cell/2, cell/2, cell/2, 1, false)
|
|
|
|
case 9:
|
|
// Shape 9: Rectangle with rectangular cutout
|
|
inner := cell * centerShapeRect9InnerRatio
|
|
var outer float64
|
|
if cell < smallCellThreshold4 {
|
|
outer = 1
|
|
} else if cell < smallCellThreshold6 {
|
|
outer = 2
|
|
} else {
|
|
outer = math.Floor(cell * centerShapeOuterMarginRatio9)
|
|
}
|
|
|
|
if cell >= smallCellThreshold8 {
|
|
inner = math.Floor(inner)
|
|
}
|
|
|
|
g.AddRectangle(0, 0, cell, cell, false)
|
|
g.AddRectangle(outer, outer, cell-outer-inner, cell-outer-inner, true)
|
|
|
|
case 10:
|
|
// Shape 10: Rectangle with circular cutout
|
|
inner := cell * centerShapeOuterMarginRatio10
|
|
outer := inner * innerOuterMultiplier10
|
|
|
|
g.AddRectangle(0, 0, cell, cell, false)
|
|
g.AddCircle(outer, outer, cell-inner-outer, true)
|
|
|
|
case 11:
|
|
// Shape 11: Small triangle (same as 7)
|
|
g.AddTriangle(cell/2, cell/2, cell/2, cell/2, 3, false)
|
|
|
|
case 12:
|
|
// Shape 12: Rectangle with rhombus cutout
|
|
m := cell * centerShapeRhombusCutoutRatio
|
|
g.AddRectangle(0, 0, cell, cell, false)
|
|
g.AddRhombus(m, m, cell-m, cell-m, true)
|
|
|
|
case 13:
|
|
// Shape 13: Large circle (only for position 0)
|
|
if positionIndex == 0 {
|
|
m := cell * centerShapeLargeCircleMarginRatio
|
|
w := cell * centerShapeLargeCircleWidthRatio
|
|
g.AddCircle(m, m, w, false)
|
|
}
|
|
}
|
|
}
|
|
|
|
// RenderOuterShape renders one of the 4 distinct outer shape patterns
|
|
func RenderOuterShape(g *Graphics, shapeIndex int, cell float64) {
|
|
index := shapeIndex % 4
|
|
|
|
switch index {
|
|
case 0:
|
|
// Shape 0: Triangle
|
|
g.AddTriangle(0, 0, cell, cell, 0, false)
|
|
|
|
case 1:
|
|
// Shape 1: Triangle (different orientation)
|
|
g.AddTriangle(0, cell/2, cell, cell/2, 0, false)
|
|
|
|
case 2:
|
|
// Shape 2: Rhombus
|
|
g.AddRhombus(0, 0, cell, cell, false)
|
|
|
|
case 3:
|
|
// Shape 3: Circle
|
|
m := cell * outerShapeCircleMarginRatio
|
|
g.AddCircle(m, m, cell-2*m, false)
|
|
}
|
|
}
|