reorganized the project
This commit is contained in:
@@ -15,10 +15,6 @@ const (
|
|||||||
buildingSizePercentStep = 0.5
|
buildingSizePercentStep = 0.5
|
||||||
)
|
)
|
||||||
|
|
||||||
func averageImageDimension(width, height int) float64 {
|
|
||||||
return (float64(width) + float64(height)) / 2.0
|
|
||||||
}
|
|
||||||
|
|
||||||
func clampBuildingSizePercent(v float64) float64 {
|
func clampBuildingSizePercent(v float64) float64 {
|
||||||
if v < minBuildingSizePercent {
|
if v < minBuildingSizePercent {
|
||||||
return minBuildingSizePercent
|
return minBuildingSizePercent
|
||||||
@@ -353,65 +349,6 @@ func getProceduralBuildingPixels(center image.Point, size float64, settings *Set
|
|||||||
return finalPixels, true
|
return finalPixels, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// scalePixels scales the building to the final size.
|
|
||||||
func scalePixels(pixels []image.Point, finalSize float64) []image.Point {
|
|
||||||
if len(pixels) == 0 {
|
|
||||||
return pixels
|
|
||||||
}
|
|
||||||
|
|
||||||
minX, minY := pixels[0].X, pixels[0].Y
|
|
||||||
maxX, maxY := pixels[0].X, pixels[0].Y
|
|
||||||
for _, p := range pixels {
|
|
||||||
if p.X < minX {
|
|
||||||
minX = p.X
|
|
||||||
}
|
|
||||||
if p.Y < minY {
|
|
||||||
minY = p.Y
|
|
||||||
}
|
|
||||||
if p.X > maxX {
|
|
||||||
maxX = p.X
|
|
||||||
}
|
|
||||||
if p.Y > maxY {
|
|
||||||
maxY = p.Y
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate the current dimensions
|
|
||||||
currentWidth := float64(maxX - minX)
|
|
||||||
currentHeight := float64(maxY - minY)
|
|
||||||
|
|
||||||
scale := finalSize / math.Max(currentWidth, currentHeight)
|
|
||||||
|
|
||||||
// Calculate the center of the bounding box
|
|
||||||
centerX := float64(minX) + currentWidth/2
|
|
||||||
centerY := float64(minY) + currentHeight/2
|
|
||||||
|
|
||||||
// Scale and translate the pixels
|
|
||||||
var scaledPixels []image.Point
|
|
||||||
pixelMap := make(map[image.Point]bool) // To avoid duplicate pixels
|
|
||||||
for _, p := range pixels {
|
|
||||||
// Translate to origin
|
|
||||||
translatedX := float64(p.X) - centerX
|
|
||||||
translatedY := float64(p.Y) - centerY
|
|
||||||
|
|
||||||
// Scale
|
|
||||||
scaledX := translatedX * scale
|
|
||||||
scaledY := translatedY * scale
|
|
||||||
|
|
||||||
// Translate back to the center
|
|
||||||
finalX := int(math.Round(scaledX + centerX))
|
|
||||||
finalY := int(math.Round(scaledY + centerY))
|
|
||||||
|
|
||||||
newPoint := image.Point{X: finalX, Y: finalY}
|
|
||||||
if !pixelMap[newPoint] {
|
|
||||||
scaledPixels = append(scaledPixels, newPoint)
|
|
||||||
pixelMap[newPoint] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return scaledPixels
|
|
||||||
}
|
|
||||||
|
|
||||||
// getComponentPixels generates the pixels for a single shape component without collision checks.
|
// getComponentPixels generates the pixels for a single shape component without collision checks.
|
||||||
func getComponentPixels(center image.Point, size float64, shape string, randSrc *rand.Rand) ([]image.Point, bool) {
|
func getComponentPixels(center image.Point, size float64, shape string, randSrc *rand.Rand) ([]image.Point, bool) {
|
||||||
var pixels []image.Point
|
var pixels []image.Point
|
||||||
@@ -564,16 +501,6 @@ func getBuildingPixels(center image.Point, size float64, shape string, waterMask
|
|||||||
return pixels, true
|
return pixels, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// isPixelInSlice checks if a pixel is already in a slice of pixels.
|
|
||||||
func isPixelInSlice(pixel image.Point, pixelSlice []image.Point) bool {
|
|
||||||
for _, p := range pixelSlice {
|
|
||||||
if p == pixel {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
u8BufferPool = sync.Pool{
|
u8BufferPool = sync.Pool{
|
||||||
New: func() any { return make([]uint8, 0) },
|
New: func() any { return make([]uint8, 0) },
|
||||||
|
|||||||
@@ -1,182 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
|
||||||
"fyne.io/fyne/v2/container"
|
|
||||||
"fyne.io/fyne/v2/data/binding"
|
|
||||||
"fyne.io/fyne/v2/layout"
|
|
||||||
"fyne.io/fyne/v2/widget"
|
|
||||||
)
|
|
||||||
|
|
||||||
type tappableImage struct {
|
|
||||||
widget.BaseWidget
|
|
||||||
image *fyne.Container
|
|
||||||
onTapped func()
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTappableImage(img *fyne.Container, tapped func()) *tappableImage {
|
|
||||||
ti := &tappableImage{
|
|
||||||
image: img,
|
|
||||||
onTapped: tapped,
|
|
||||||
}
|
|
||||||
ti.ExtendBaseWidget(ti)
|
|
||||||
return ti
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tappableImage) CreateRenderer() fyne.WidgetRenderer {
|
|
||||||
return widget.NewSimpleRenderer(t.image)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tappableImage) Tapped(*fyne.PointEvent) {
|
|
||||||
if t.onTapped != nil {
|
|
||||||
t.onTapped()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// numericInputSlider combines a slider and text entry for numeric input
|
|
||||||
type numericInputSlider struct {
|
|
||||||
widget.BaseWidget
|
|
||||||
value binding.Float
|
|
||||||
min, max float64
|
|
||||||
step float64
|
|
||||||
slider *widget.Slider
|
|
||||||
entry *widget.Entry
|
|
||||||
format string
|
|
||||||
errorLabel *widget.Label
|
|
||||||
label *widget.Label
|
|
||||||
}
|
|
||||||
|
|
||||||
type numericInputSliderRenderer struct {
|
|
||||||
slider *numericInputSlider
|
|
||||||
label *widget.Label
|
|
||||||
entry *widget.Entry
|
|
||||||
sliderWidget *widget.Slider
|
|
||||||
errorLabel *widget.Label
|
|
||||||
layout fyne.Layout
|
|
||||||
objects []fyne.CanvasObject
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *numericInputSliderRenderer) MinSize() fyne.Size {
|
|
||||||
return r.layout.MinSize(r.objects)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *numericInputSliderRenderer) Layout(size fyne.Size) {
|
|
||||||
r.layout.Layout(r.objects, size)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *numericInputSliderRenderer) Objects() []fyne.CanvasObject {
|
|
||||||
return r.objects
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *numericInputSliderRenderer) Refresh() {
|
|
||||||
r.label.SetText(r.slider.label.Text)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *numericInputSliderRenderer) Destroy() {}
|
|
||||||
|
|
||||||
// newNumericInputSlider creates a new numericInputSlider widget
|
|
||||||
func newNumericInputSlider(min, max float64, initialValue float64, format string, labelText string) *numericInputSlider {
|
|
||||||
s := &numericInputSlider{
|
|
||||||
min: min,
|
|
||||||
max: max,
|
|
||||||
format: format,
|
|
||||||
step: 0,
|
|
||||||
}
|
|
||||||
s.ExtendBaseWidget(s)
|
|
||||||
|
|
||||||
s.label = widget.NewLabel(labelText)
|
|
||||||
s.value = binding.NewFloat()
|
|
||||||
s.value.Set(initialValue)
|
|
||||||
|
|
||||||
s.slider = widget.NewSlider(min, max)
|
|
||||||
s.slider.Bind(s.value)
|
|
||||||
|
|
||||||
s.entry = widget.NewEntry()
|
|
||||||
s.value.AddListener(binding.NewDataListener(func() {
|
|
||||||
val, _ := s.value.Get()
|
|
||||||
s.entry.SetText(fmt.Sprintf(s.format, val))
|
|
||||||
}))
|
|
||||||
|
|
||||||
s.errorLabel = widget.NewLabel("")
|
|
||||||
s.errorLabel.Hide()
|
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func newNumericInputSliderWithStep(min, max, initialValue, step float64, format string, labelText string) *numericInputSlider {
|
|
||||||
s := newNumericInputSlider(min, max, initialValue, format, labelText)
|
|
||||||
if step > 0 {
|
|
||||||
s.step = step
|
|
||||||
s.slider.Step = step
|
|
||||||
rounded := min + math.Round((initialValue-min)/step)*step
|
|
||||||
s.value.Set(rounded)
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate checks text entry for valid numeric input within the defined range
|
|
||||||
func (s *numericInputSlider) validate(text string, onError func(bool)) {
|
|
||||||
text = strings.TrimSpace(text)
|
|
||||||
text = strings.TrimSuffix(text, "px")
|
|
||||||
text = strings.TrimSuffix(text, "%")
|
|
||||||
text = strings.TrimSuffix(text, "°")
|
|
||||||
text = strings.TrimSpace(text)
|
|
||||||
val, err := strconv.ParseFloat(text, 64)
|
|
||||||
if err != nil {
|
|
||||||
s.errorLabel.SetText("Not a number")
|
|
||||||
s.errorLabel.Show()
|
|
||||||
onError(true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if val < s.min || val > s.max {
|
|
||||||
s.errorLabel.SetText(fmt.Sprintf(
|
|
||||||
"Out of range (%s-%s)",
|
|
||||||
strconv.FormatFloat(s.min, 'f', -1, 64),
|
|
||||||
strconv.FormatFloat(s.max, 'f', -1, 64),
|
|
||||||
))
|
|
||||||
s.errorLabel.Show()
|
|
||||||
onError(true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if s.step > 0 {
|
|
||||||
steps := math.Round((val - s.min) / s.step)
|
|
||||||
snapped := s.min + steps*s.step
|
|
||||||
if math.Abs(val-snapped) > 1e-9 {
|
|
||||||
s.errorLabel.SetText(fmt.Sprintf("Use increments of %s", strconv.FormatFloat(s.step, 'f', -1, 64)))
|
|
||||||
s.errorLabel.Show()
|
|
||||||
onError(true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val = snapped
|
|
||||||
}
|
|
||||||
|
|
||||||
s.errorLabel.Hide()
|
|
||||||
onError(false)
|
|
||||||
s.value.Set(val)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateRenderer renders the widget as required by Fyne toolkit
|
|
||||||
func (s *numericInputSlider) CreateRenderer() fyne.WidgetRenderer {
|
|
||||||
r := &numericInputSliderRenderer{
|
|
||||||
slider: s,
|
|
||||||
label: s.label,
|
|
||||||
entry: s.entry,
|
|
||||||
sliderWidget: s.slider,
|
|
||||||
errorLabel: s.errorLabel,
|
|
||||||
}
|
|
||||||
|
|
||||||
r.layout = layout.NewGridLayout(1)
|
|
||||||
r.objects = []fyne.CanvasObject{
|
|
||||||
container.NewGridWithColumns(2, r.label, r.entry),
|
|
||||||
r.sliderWidget,
|
|
||||||
r.errorLabel,
|
|
||||||
}
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
+9
-76
@@ -91,13 +91,13 @@ func getTurretSizePixels(settings *Settings, width, height int) float64 {
|
|||||||
|
|
||||||
// GateInfo describes a single gate in a wall ring.
|
// GateInfo describes a single gate in a wall ring.
|
||||||
type GateInfo struct {
|
type GateInfo struct {
|
||||||
WallID int
|
WallID int
|
||||||
Center image.Point // midpoint of the gap
|
Center image.Point // midpoint of the gap
|
||||||
Normal [2]float64 // outward normal (perpendicular to wall, pointing outward)
|
Normal [2]float64 // outward normal (perpendicular to wall, pointing outward)
|
||||||
LeftTurret image.Point // turret on the left side of the road
|
LeftTurret image.Point // turret on the left side of the road
|
||||||
RightTurret image.Point // turret on the right side of the road
|
RightTurret image.Point // turret on the right side of the road
|
||||||
InnerEnd image.Point // road endpoint just inside the wall
|
InnerEnd image.Point // road endpoint just inside the wall
|
||||||
OuterEnd image.Point // road endpoint just outside the wall
|
OuterEnd image.Point // road endpoint just outside the wall
|
||||||
}
|
}
|
||||||
|
|
||||||
type FortificationLayout struct {
|
type FortificationLayout struct {
|
||||||
@@ -255,7 +255,7 @@ func computeGatesForLayout(
|
|||||||
gateCenter := bpts[i].p
|
gateCenter := bpts[i].p
|
||||||
|
|
||||||
// Estimate wall tangent and normal at this point.
|
// Estimate wall tangent and normal at this point.
|
||||||
tx, ty, ok := fortEstimateWallTangent(gateCenter, layout.Mask)
|
tx, ty, ok := estimateWallTangent(gateCenter, layout.Mask)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -658,15 +658,6 @@ func drawSegmentSelective(x0, y0, x1, y1 int, plot func(x, y int)) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func cloneMask(src *PixelMask) *PixelMask {
|
|
||||||
if src == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
dst := NewPixelMask(src.Width, src.Height)
|
|
||||||
copy(dst.Data, src.Data)
|
|
||||||
return dst
|
|
||||||
}
|
|
||||||
|
|
||||||
func drawWallMask(img *image.RGBA, wallMask *PixelMask) {
|
func drawWallMask(img *image.RGBA, wallMask *PixelMask) {
|
||||||
if img == nil || wallMask == nil {
|
if img == nil || wallMask == nil {
|
||||||
return
|
return
|
||||||
@@ -792,7 +783,7 @@ func GenerateTurrets(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, g := range gates {
|
for _, g := range gates {
|
||||||
tx, ty, ok := fortEstimateWallTangent(g, layout.Mask)
|
tx, ty, ok := estimateWallTangent(g, layout.Mask)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -855,18 +846,6 @@ func touchesWater(x, y int, waterMask *PixelMask) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func averagePoint(points []image.Point) image.Point {
|
|
||||||
if len(points) == 0 {
|
|
||||||
return image.Point{}
|
|
||||||
}
|
|
||||||
var sx, sy int
|
|
||||||
for _, p := range points {
|
|
||||||
sx += p.X
|
|
||||||
sy += p.Y
|
|
||||||
}
|
|
||||||
return image.Point{X: sx / len(points), Y: sy / len(points)}
|
|
||||||
}
|
|
||||||
|
|
||||||
func drawTurret(img *image.RGBA, mask *PixelMask, center image.Point, radius int, shape string, col color.RGBA) {
|
func drawTurret(img *image.RGBA, mask *PixelMask, center image.Point, radius int, shape string, col color.RGBA) {
|
||||||
for dy := -radius; dy <= radius; dy++ {
|
for dy := -radius; dy <= radius; dy++ {
|
||||||
for dx := -radius; dx <= radius; dx++ {
|
for dx := -radius; dx <= radius; dx++ {
|
||||||
@@ -1029,49 +1008,3 @@ func roadGateCentersForRoad(r *Road, layout *FortificationLayout) []image.Point
|
|||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func fortEstimateWallTangent(mid image.Point, wallMask *PixelMask) (float64, float64, bool) {
|
|
||||||
if wallMask == nil {
|
|
||||||
return 0, 0, false
|
|
||||||
}
|
|
||||||
const r = 4
|
|
||||||
var pts [][2]float64
|
|
||||||
for dy := -r; dy <= r; dy++ {
|
|
||||||
y := mid.Y + dy
|
|
||||||
if y < 0 || y >= wallMask.Height {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for dx := -r; dx <= r; dx++ {
|
|
||||||
x := mid.X + dx
|
|
||||||
if x < 0 || x >= wallMask.Width {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if wallMask.GetXY(x, y) {
|
|
||||||
pts = append(pts, [2]float64{float64(x), float64(y)})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(pts) < 3 {
|
|
||||||
return 0, 0, false
|
|
||||||
}
|
|
||||||
var mx, my float64
|
|
||||||
for _, p := range pts {
|
|
||||||
mx += p[0]
|
|
||||||
my += p[1]
|
|
||||||
}
|
|
||||||
mx /= float64(len(pts))
|
|
||||||
my /= float64(len(pts))
|
|
||||||
var sxx, syy, sxy float64
|
|
||||||
for _, p := range pts {
|
|
||||||
dx := p[0] - mx
|
|
||||||
dy := p[1] - my
|
|
||||||
sxx += dx * dx
|
|
||||||
syy += dy * dy
|
|
||||||
sxy += dx * dy
|
|
||||||
}
|
|
||||||
if sxx+syy < 0.001 {
|
|
||||||
return 0, 0, false
|
|
||||||
}
|
|
||||||
theta := 0.5 * math.Atan2(2*sxy, sxx-syy)
|
|
||||||
return math.Cos(theta), math.Sin(theta), true
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import "image"
|
|
||||||
|
|
||||||
// PixelMask stores per-pixel occupancy using one byte per pixel.
|
|
||||||
type PixelMask struct {
|
|
||||||
Width int
|
|
||||||
Height int
|
|
||||||
Data []uint8
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewPixelMask(width, height int) *PixelMask {
|
|
||||||
if width <= 0 || height <= 0 {
|
|
||||||
return &PixelMask{}
|
|
||||||
}
|
|
||||||
return &PixelMask{
|
|
||||||
Width: width,
|
|
||||||
Height: height,
|
|
||||||
Data: make([]uint8, width*height),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *PixelMask) index(x, y int) int {
|
|
||||||
return y*m.Width + x
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *PixelMask) InBounds(x, y int) bool {
|
|
||||||
return m != nil && x >= 0 && y >= 0 && x < m.Width && y < m.Height
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *PixelMask) GetXY(x, y int) bool {
|
|
||||||
if !m.InBounds(x, y) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return m.Data[m.index(x, y)] != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *PixelMask) SetXY(x, y int) {
|
|
||||||
if m.InBounds(x, y) {
|
|
||||||
m.Data[m.index(x, y)] = 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *PixelMask) ClearXY(x, y int) {
|
|
||||||
if m.InBounds(x, y) {
|
|
||||||
m.Data[m.index(x, y)] = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *PixelMask) GetPoint(p image.Point) bool {
|
|
||||||
return m.GetXY(p.X, p.Y)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *PixelMask) SetPoint(p image.Point) {
|
|
||||||
m.SetXY(p.X, p.Y)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *PixelMask) Merge(other *PixelMask) {
|
|
||||||
if m == nil || other == nil || m.Width != other.Width || m.Height != other.Height {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for i := range m.Data {
|
|
||||||
if other.Data[i] != 0 {
|
|
||||||
m.Data[i] = 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *PixelMask) AddPoints(points []image.Point) {
|
|
||||||
for _, p := range points {
|
|
||||||
m.SetPoint(p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *PixelMask) ToPoints() []image.Point {
|
|
||||||
if m == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
pts := make([]image.Point, 0)
|
|
||||||
for y := 0; y < m.Height; y++ {
|
|
||||||
row := y * m.Width
|
|
||||||
for x := 0; x < m.Width; x++ {
|
|
||||||
if m.Data[row+x] != 0 {
|
|
||||||
pts = append(pts, image.Point{X: x, Y: y})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pts
|
|
||||||
}
|
|
||||||
|
|
||||||
func BuildMaskFromLakes(width, height int, lakes [][]image.Point) *PixelMask {
|
|
||||||
mask := NewPixelMask(width, height)
|
|
||||||
for _, lake := range lakes {
|
|
||||||
mask.AddPoints(lake)
|
|
||||||
}
|
|
||||||
return mask
|
|
||||||
}
|
|
||||||
@@ -1277,54 +1277,6 @@ func enforcePerpendicularWallCrossing(points []PathPoint, start, wallStart, wall
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func estimateWallTangent(mid image.Point, wallMask *PixelMask) (float64, float64, bool) {
|
|
||||||
if wallMask == nil {
|
|
||||||
return 0, 0, false
|
|
||||||
}
|
|
||||||
const r = 4
|
|
||||||
var pts [][2]float64
|
|
||||||
for dy := -r; dy <= r; dy++ {
|
|
||||||
y := mid.Y + dy
|
|
||||||
if y < 0 || y >= wallMask.Height {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for dx := -r; dx <= r; dx++ {
|
|
||||||
x := mid.X + dx
|
|
||||||
if x < 0 || x >= wallMask.Width {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if wallMask.GetXY(x, y) {
|
|
||||||
pts = append(pts, [2]float64{float64(x), float64(y)})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(pts) < 3 {
|
|
||||||
return 0, 0, false
|
|
||||||
}
|
|
||||||
|
|
||||||
var mx, my float64
|
|
||||||
for _, p := range pts {
|
|
||||||
mx += p[0]
|
|
||||||
my += p[1]
|
|
||||||
}
|
|
||||||
mx /= float64(len(pts))
|
|
||||||
my /= float64(len(pts))
|
|
||||||
|
|
||||||
var sxx, syy, sxy float64
|
|
||||||
for _, p := range pts {
|
|
||||||
dx := p[0] - mx
|
|
||||||
dy := p[1] - my
|
|
||||||
sxx += dx * dx
|
|
||||||
syy += dy * dy
|
|
||||||
sxy += dx * dy
|
|
||||||
}
|
|
||||||
if sxx+syy < 0.001 {
|
|
||||||
return 0, 0, false
|
|
||||||
}
|
|
||||||
theta := 0.5 * math.Atan2(2*sxy, sxx-syy)
|
|
||||||
return math.Cos(theta), math.Sin(theta), true
|
|
||||||
}
|
|
||||||
|
|
||||||
func crossingAngleToTangentDegrees(rx, ry, tx, ty float64) float64 {
|
func crossingAngleToTangentDegrees(rx, ry, tx, ty float64) float64 {
|
||||||
rn := math.Hypot(rx, ry)
|
rn := math.Hypot(rx, ry)
|
||||||
tn := math.Hypot(tx, ty)
|
tn := math.Hypot(tx, ty)
|
||||||
@@ -1958,21 +1910,3 @@ func ensureGateRoadConnections(gateRoads []*Road, allRoads []*Road, wallLayout *
|
|||||||
}
|
}
|
||||||
return append(allRoads, connectors...)
|
return append(allRoads, connectors...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func clamp(v, lo, hi float64) float64 {
|
|
||||||
if v < lo {
|
|
||||||
return lo
|
|
||||||
}
|
|
||||||
if v > hi {
|
|
||||||
return hi
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// abs returns the absolute value of an integer.
|
|
||||||
func abs(x int) int {
|
|
||||||
if x < 0 {
|
|
||||||
return -x
|
|
||||||
}
|
|
||||||
return x
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import "math/rand"
|
|
||||||
|
|
||||||
// SeedProvider provides a stream of random seeds from a single initial seed
|
|
||||||
type SeedProvider struct {
|
|
||||||
rand *rand.Rand
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSeedProvider creates a new SeedProvider with the given initial seed
|
|
||||||
func NewSeedProvider(seed int64) *SeedProvider {
|
|
||||||
return &SeedProvider{
|
|
||||||
rand: rand.New(rand.NewSource(seed)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next returns the next random seed in the sequence
|
|
||||||
func (sp *SeedProvider) Next() int64 {
|
|
||||||
return sp.rand.Int63()
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,412 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"math"
|
||||||
|
"math/rand"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/container"
|
||||||
|
"fyne.io/fyne/v2/data/binding"
|
||||||
|
"fyne.io/fyne/v2/layout"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PixelMask stores per-pixel occupancy for placement and collision checks.
|
||||||
|
type PixelMask struct {
|
||||||
|
Width int
|
||||||
|
Height int
|
||||||
|
Data []uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPixelMask creates a mask for width and height and is used for feature occupancy.
|
||||||
|
func NewPixelMask(width, height int) *PixelMask {
|
||||||
|
if width <= 0 || height <= 0 {
|
||||||
|
return &PixelMask{}
|
||||||
|
}
|
||||||
|
return &PixelMask{
|
||||||
|
Width: width,
|
||||||
|
Height: height,
|
||||||
|
Data: make([]uint8, width*height),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// index returns the slice index for coordinates and is used by mask helpers.
|
||||||
|
func (m *PixelMask) index(x, y int) int {
|
||||||
|
return y*m.Width + x
|
||||||
|
}
|
||||||
|
|
||||||
|
// InBounds reports whether x,y are inside the mask and is used to guard access.
|
||||||
|
func (m *PixelMask) InBounds(x, y int) bool {
|
||||||
|
return m != nil && x >= 0 && y >= 0 && x < m.Width && y < m.Height
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetXY returns occupancy at x,y and is used for collision checks.
|
||||||
|
func (m *PixelMask) GetXY(x, y int) bool {
|
||||||
|
if !m.InBounds(x, y) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return m.Data[m.index(x, y)] != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetXY marks x,y as occupied and is used when placing features.
|
||||||
|
func (m *PixelMask) SetXY(x, y int) {
|
||||||
|
if m.InBounds(x, y) {
|
||||||
|
m.Data[m.index(x, y)] = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearXY clears occupancy at x,y and is used for mask edits.
|
||||||
|
func (m *PixelMask) ClearXY(x, y int) {
|
||||||
|
if m.InBounds(x, y) {
|
||||||
|
m.Data[m.index(x, y)] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPoint returns occupancy at a point and is used with image.Point helpers.
|
||||||
|
func (m *PixelMask) GetPoint(p image.Point) bool {
|
||||||
|
return m.GetXY(p.X, p.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPoint marks a point as occupied and is used with image.Point helpers.
|
||||||
|
func (m *PixelMask) SetPoint(p image.Point) {
|
||||||
|
m.SetXY(p.X, p.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge ORs another mask into this one and is used to combine feature masks.
|
||||||
|
func (m *PixelMask) Merge(other *PixelMask) {
|
||||||
|
if m == nil || other == nil || m.Width != other.Width || m.Height != other.Height {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i := range m.Data {
|
||||||
|
if other.Data[i] != 0 {
|
||||||
|
m.Data[i] = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddPoints marks a slice of points as occupied and is used for lake/road masks.
|
||||||
|
func (m *PixelMask) AddPoints(points []image.Point) {
|
||||||
|
for _, p := range points {
|
||||||
|
m.SetPoint(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToPoints returns occupied points and is used to extract anchors.
|
||||||
|
func (m *PixelMask) ToPoints() []image.Point {
|
||||||
|
if m == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
pts := make([]image.Point, 0)
|
||||||
|
for y := 0; y < m.Height; y++ {
|
||||||
|
row := y * m.Width
|
||||||
|
for x := 0; x < m.Width; x++ {
|
||||||
|
if m.Data[row+x] != 0 {
|
||||||
|
pts = append(pts, image.Point{X: x, Y: y})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pts
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildMaskFromLakes builds a water mask from lake point sets for later merges.
|
||||||
|
func BuildMaskFromLakes(width, height int, lakes [][]image.Point) *PixelMask {
|
||||||
|
mask := NewPixelMask(width, height)
|
||||||
|
for _, lake := range lakes {
|
||||||
|
mask.AddPoints(lake)
|
||||||
|
}
|
||||||
|
return mask
|
||||||
|
}
|
||||||
|
|
||||||
|
// SeedProvider provides a deterministic stream of seeds for generation steps.
|
||||||
|
type SeedProvider struct {
|
||||||
|
rand *rand.Rand
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSeedProvider creates a SeedProvider for splitting a root seed into steps.
|
||||||
|
func NewSeedProvider(seed int64) *SeedProvider {
|
||||||
|
return &SeedProvider{
|
||||||
|
rand: rand.New(rand.NewSource(seed)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next returns the next random seed and is used to drive independent generators.
|
||||||
|
func (sp *SeedProvider) Next() int64 {
|
||||||
|
return sp.rand.Int63()
|
||||||
|
}
|
||||||
|
|
||||||
|
// averageImageDimension returns (width+height)/2 and is used for percent scaling.
|
||||||
|
func averageImageDimension(width, height int) float64 {
|
||||||
|
return (float64(width) + float64(height)) / 2.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// clamp clamps v to [lo, hi] and is used for settings normalization.
|
||||||
|
func clamp(v, lo, hi float64) float64 {
|
||||||
|
if v < lo {
|
||||||
|
return lo
|
||||||
|
}
|
||||||
|
if v > hi {
|
||||||
|
return hi
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// clamp01 clamps v to [0,1] and is used for normalized weights.
|
||||||
|
func clamp01(v float64) float64 {
|
||||||
|
return clamp(v, 0, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// abs returns the absolute value of an int and is used in raster helpers.
|
||||||
|
func abs(x int) int {
|
||||||
|
if x < 0 {
|
||||||
|
return -x
|
||||||
|
}
|
||||||
|
return x
|
||||||
|
}
|
||||||
|
|
||||||
|
// cloneMask copies a PixelMask and is used when mutating temporary masks.
|
||||||
|
func cloneMask(src *PixelMask) *PixelMask {
|
||||||
|
if src == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
dst := NewPixelMask(src.Width, src.Height)
|
||||||
|
copy(dst.Data, src.Data)
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
|
// averagePoint returns the average of points and is used for centroids.
|
||||||
|
func averagePoint(points []image.Point) image.Point {
|
||||||
|
if len(points) == 0 {
|
||||||
|
return image.Point{}
|
||||||
|
}
|
||||||
|
var sx, sy int
|
||||||
|
for _, p := range points {
|
||||||
|
sx += p.X
|
||||||
|
sy += p.Y
|
||||||
|
}
|
||||||
|
return image.Point{X: sx / len(points), Y: sy / len(points)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// estimateWallTangent approximates the tangent at a wall pixel and is used to align roads and gates.
|
||||||
|
func estimateWallTangent(mid image.Point, wallMask *PixelMask) (float64, float64, bool) {
|
||||||
|
if wallMask == nil {
|
||||||
|
return 0, 0, false
|
||||||
|
}
|
||||||
|
const r = 4
|
||||||
|
var pts [][2]float64
|
||||||
|
for dy := -r; dy <= r; dy++ {
|
||||||
|
y := mid.Y + dy
|
||||||
|
if y < 0 || y >= wallMask.Height {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for dx := -r; dx <= r; dx++ {
|
||||||
|
x := mid.X + dx
|
||||||
|
if x < 0 || x >= wallMask.Width {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if wallMask.GetXY(x, y) {
|
||||||
|
pts = append(pts, [2]float64{float64(x), float64(y)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(pts) < 3 {
|
||||||
|
return 0, 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
var mx, my float64
|
||||||
|
for _, p := range pts {
|
||||||
|
mx += p[0]
|
||||||
|
my += p[1]
|
||||||
|
}
|
||||||
|
mx /= float64(len(pts))
|
||||||
|
my /= float64(len(pts))
|
||||||
|
|
||||||
|
var sxx, syy, sxy float64
|
||||||
|
for _, p := range pts {
|
||||||
|
dx := p[0] - mx
|
||||||
|
dy := p[1] - my
|
||||||
|
sxx += dx * dx
|
||||||
|
syy += dy * dy
|
||||||
|
sxy += dx * dy
|
||||||
|
}
|
||||||
|
if sxx+syy < 0.001 {
|
||||||
|
return 0, 0, false
|
||||||
|
}
|
||||||
|
theta := 0.5 * math.Atan2(2*sxy, sxx-syy)
|
||||||
|
return math.Cos(theta), math.Sin(theta), true
|
||||||
|
}
|
||||||
|
|
||||||
|
type tappableImage struct {
|
||||||
|
widget.BaseWidget
|
||||||
|
image *fyne.Container
|
||||||
|
onTapped func()
|
||||||
|
}
|
||||||
|
|
||||||
|
// newTappableImage wraps a container so taps can trigger image interactions.
|
||||||
|
func newTappableImage(img *fyne.Container, tapped func()) *tappableImage {
|
||||||
|
ti := &tappableImage{
|
||||||
|
image: img,
|
||||||
|
onTapped: tapped,
|
||||||
|
}
|
||||||
|
ti.ExtendBaseWidget(ti)
|
||||||
|
return ti
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateRenderer builds the renderer for tappableImage and is used by Fyne.
|
||||||
|
func (t *tappableImage) CreateRenderer() fyne.WidgetRenderer {
|
||||||
|
return widget.NewSimpleRenderer(t.image)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tapped invokes the handler and is used for click interactions.
|
||||||
|
func (t *tappableImage) Tapped(*fyne.PointEvent) {
|
||||||
|
if t.onTapped != nil {
|
||||||
|
t.onTapped()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// numericInputSlider combines a slider and text entry for numeric input.
|
||||||
|
type numericInputSlider struct {
|
||||||
|
widget.BaseWidget
|
||||||
|
value binding.Float
|
||||||
|
min, max float64
|
||||||
|
step float64
|
||||||
|
slider *widget.Slider
|
||||||
|
entry *widget.Entry
|
||||||
|
format string
|
||||||
|
errorLabel *widget.Label
|
||||||
|
label *widget.Label
|
||||||
|
}
|
||||||
|
|
||||||
|
type numericInputSliderRenderer struct {
|
||||||
|
slider *numericInputSlider
|
||||||
|
label *widget.Label
|
||||||
|
entry *widget.Entry
|
||||||
|
sliderWidget *widget.Slider
|
||||||
|
errorLabel *widget.Label
|
||||||
|
layout fyne.Layout
|
||||||
|
objects []fyne.CanvasObject
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *numericInputSliderRenderer) MinSize() fyne.Size {
|
||||||
|
return r.layout.MinSize(r.objects)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *numericInputSliderRenderer) Layout(size fyne.Size) {
|
||||||
|
r.layout.Layout(r.objects, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *numericInputSliderRenderer) Objects() []fyne.CanvasObject {
|
||||||
|
return r.objects
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *numericInputSliderRenderer) Refresh() {
|
||||||
|
r.label.SetText(r.slider.label.Text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *numericInputSliderRenderer) Destroy() {}
|
||||||
|
|
||||||
|
// newNumericInputSlider creates a slider+entry pair for numeric settings.
|
||||||
|
func newNumericInputSlider(min, max float64, initialValue float64, format string, labelText string) *numericInputSlider {
|
||||||
|
s := &numericInputSlider{
|
||||||
|
min: min,
|
||||||
|
max: max,
|
||||||
|
format: format,
|
||||||
|
step: 0,
|
||||||
|
}
|
||||||
|
s.ExtendBaseWidget(s)
|
||||||
|
|
||||||
|
s.label = widget.NewLabel(labelText)
|
||||||
|
s.value = binding.NewFloat()
|
||||||
|
s.value.Set(initialValue)
|
||||||
|
|
||||||
|
s.slider = widget.NewSlider(min, max)
|
||||||
|
s.slider.Bind(s.value)
|
||||||
|
|
||||||
|
s.entry = widget.NewEntry()
|
||||||
|
s.value.AddListener(binding.NewDataListener(func() {
|
||||||
|
val, _ := s.value.Get()
|
||||||
|
s.entry.SetText(fmt.Sprintf(s.format, val))
|
||||||
|
}))
|
||||||
|
|
||||||
|
s.errorLabel = widget.NewLabel("")
|
||||||
|
s.errorLabel.Hide()
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// newNumericInputSliderWithStep creates a numeric slider that snaps to increments.
|
||||||
|
func newNumericInputSliderWithStep(min, max, initialValue, step float64, format string, labelText string) *numericInputSlider {
|
||||||
|
s := newNumericInputSlider(min, max, initialValue, format, labelText)
|
||||||
|
if step > 0 {
|
||||||
|
s.step = step
|
||||||
|
s.slider.Step = step
|
||||||
|
rounded := min + math.Round((initialValue-min)/step)*step
|
||||||
|
s.value.Set(rounded)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate checks entry input and is used for slider text validation.
|
||||||
|
func (s *numericInputSlider) validate(text string, onError func(bool)) {
|
||||||
|
text = strings.TrimSpace(text)
|
||||||
|
text = strings.TrimSuffix(text, "px")
|
||||||
|
text = strings.TrimSuffix(text, "%")
|
||||||
|
text = strings.TrimSuffix(text, "°")
|
||||||
|
text = strings.TrimSpace(text)
|
||||||
|
val, err := strconv.ParseFloat(text, 64)
|
||||||
|
if err != nil {
|
||||||
|
s.errorLabel.SetText("Not a number")
|
||||||
|
s.errorLabel.Show()
|
||||||
|
onError(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if val < s.min || val > s.max {
|
||||||
|
s.errorLabel.SetText(fmt.Sprintf(
|
||||||
|
"Out of range (%s-%s)",
|
||||||
|
strconv.FormatFloat(s.min, 'f', -1, 64),
|
||||||
|
strconv.FormatFloat(s.max, 'f', -1, 64),
|
||||||
|
))
|
||||||
|
s.errorLabel.Show()
|
||||||
|
onError(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.step > 0 {
|
||||||
|
steps := math.Round((val - s.min) / s.step)
|
||||||
|
snapped := s.min + steps*s.step
|
||||||
|
if math.Abs(val-snapped) > 1e-9 {
|
||||||
|
s.errorLabel.SetText(fmt.Sprintf("Use increments of %s", strconv.FormatFloat(s.step, 'f', -1, 64)))
|
||||||
|
s.errorLabel.Show()
|
||||||
|
onError(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val = snapped
|
||||||
|
}
|
||||||
|
|
||||||
|
s.errorLabel.Hide()
|
||||||
|
onError(false)
|
||||||
|
s.value.Set(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateRenderer builds the widget layout and is used by Fyne.
|
||||||
|
func (s *numericInputSlider) CreateRenderer() fyne.WidgetRenderer {
|
||||||
|
r := &numericInputSliderRenderer{
|
||||||
|
slider: s,
|
||||||
|
label: s.label,
|
||||||
|
entry: s.entry,
|
||||||
|
sliderWidget: s.slider,
|
||||||
|
errorLabel: s.errorLabel,
|
||||||
|
}
|
||||||
|
|
||||||
|
r.layout = layout.NewGridLayout(1)
|
||||||
|
r.objects = []fyne.CanvasObject{
|
||||||
|
container.NewGridWithColumns(2, r.label, r.entry),
|
||||||
|
r.sliderWidget,
|
||||||
|
r.errorLabel,
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
@@ -874,30 +874,6 @@ func removeSpeckles(mask *[]uint8, bw, bh, minNeighbors int) {
|
|||||||
*mask = arr
|
*mask = arr
|
||||||
}
|
}
|
||||||
|
|
||||||
// maskToPoints converts mask to point slice for visualization
|
|
||||||
func maskToPoints(mask []uint8, bw, bh, minX, minY int) []image.Point {
|
|
||||||
var pts []image.Point
|
|
||||||
for y := 0; y < bh; y++ {
|
|
||||||
for x := 0; x < bw; x++ {
|
|
||||||
if mask[y*bw+x] == 1 {
|
|
||||||
pts = append(pts, image.Point{X: x + minX, Y: y + minY})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pts
|
|
||||||
}
|
|
||||||
|
|
||||||
// clamp01 clamps value to 0..1 range
|
|
||||||
func clamp01(v float64) float64 {
|
|
||||||
if v < 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
if v > 1 {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
// GenerateRivers creates rivers flowing across the map from edge to edge
|
// GenerateRivers creates rivers flowing across the map from edge to edge
|
||||||
func GenerateRivers(width, height, numRivers int, minWidth, maxWidth, curvyness float64, inputImage image.Image, lakes [][]image.Point, seed int64, heightmap image.Image, riverWidthVariability, riverEdgeRoughness float64) (image.Image, *PixelMask) {
|
func GenerateRivers(width, height, numRivers int, minWidth, maxWidth, curvyness float64, inputImage image.Image, lakes [][]image.Point, seed int64, heightmap image.Image, riverWidthVariability, riverEdgeRoughness float64) (image.Image, *PixelMask) {
|
||||||
if numRivers == 0 {
|
if numRivers == 0 {
|
||||||
@@ -959,7 +935,7 @@ func GenerateRivers(width, height, numRivers int, minWidth, maxWidth, curvyness
|
|||||||
if isWater.GetPoint(p) {
|
if isWater.GetPoint(p) {
|
||||||
if lakeIndex, isLake := lakePixelMap[p]; isLake {
|
if lakeIndex, isLake := lakePixelMap[p]; isLake {
|
||||||
// End river at lake center if it intersects
|
// End river at lake center if it intersects
|
||||||
lakeCenter := findCenter(lakes[lakeIndex])
|
lakeCenter := averagePoint(lakes[lakeIndex])
|
||||||
r.End = lakeCenter
|
r.End = lakeCenter
|
||||||
} else {
|
} else {
|
||||||
// End river at intersection with another river
|
// End river at intersection with another river
|
||||||
@@ -1086,22 +1062,6 @@ func calculateRiverPath(start, end image.Point, curvyness, avgDim float64, randS
|
|||||||
return bresenhamRiver(controlPoints)
|
return bresenhamRiver(controlPoints)
|
||||||
}
|
}
|
||||||
|
|
||||||
// findCenter calculates the center point of a set of pixels
|
|
||||||
func findCenter(pixels []image.Point) image.Point {
|
|
||||||
if len(pixels) == 0 {
|
|
||||||
return image.Point{}
|
|
||||||
}
|
|
||||||
var sumX, sumY int
|
|
||||||
for _, p := range pixels {
|
|
||||||
sumX += p.X
|
|
||||||
sumY += p.Y
|
|
||||||
}
|
|
||||||
return image.Point{
|
|
||||||
X: sumX / len(pixels),
|
|
||||||
Y: sumY / len(pixels),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getPointOnEdge returns a random point on the specified map edge
|
// getPointOnEdge returns a random point on the specified map edge
|
||||||
func getPointOnEdge(width, height, edge int, randSrc *rand.Rand) image.Point {
|
func getPointOnEdge(width, height, edge int, randSrc *rand.Rand) image.Point {
|
||||||
switch edge {
|
switch edge {
|
||||||
|
|||||||
Reference in New Issue
Block a user