-
Notifications
You must be signed in to change notification settings - Fork 1
/
trains.go
347 lines (296 loc) · 7.83 KB
/
trains.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
package main
import (
"bytes"
"flag"
"fmt"
"io"
"log"
"math"
"os"
"github.com/ajstarks/svgo"
)
var (
// AngleN tells how many curve parts form a full circle
AngleN = 12
// Radius is the radius of the circle made by curve parts
Radius = 2.0
// BridgeLen defines the length of the bridge part
BridgeLen = 8.0
// StraightLen defines the length of the straight part
StraightLen = 1.0
)
// Angle represents the rotation of a track piece.
type Angle int
// Add adds given angle to this one. The angles are added modulo 2pi.
func (a Angle) Add(b Angle) Angle {
res := (a + b) % Angle(AngleN)
if res < 0 {
res += Angle(AngleN)
}
return res
}
// Rad returns the angle in radians.
func (a Angle) Rad() float64 {
return 2 * math.Pi * float64(a) / float64(AngleN)
}
//Deg returns the angle in degrees.
func (a Angle) Deg() float64 {
return float64(a) * 360 / float64(AngleN)
}
// Kind specifies the kinds of track parts.
type Kind int
// All possible kinds of track parts.
const (
End Kind = iota
Bridge
Straight
Left
Right
kindCnt
)
func (k *Kind) String() string {
return []string{"E", "B", "S", "A", "C"}[*k]
}
// Part represents a placed part in the track
type Part struct {
X float64 // x coord of the part origin
Y float64 // y coord of the pard origin
Angle Angle // directon the part is rotated to
Kind Kind // part type
}
// AtSamePlaceAs tells whether p2 starts at the same place and has same angle as this part.
// There is some tolerance.
func (p *Part) AtSamePlaceAs(p2 *Part) bool {
const prec = 1e-2
return math.Abs(p.X-p2.X) < prec && math.Abs(p.Y-p2.Y) < prec && p.Angle == p2.Angle
}
type update struct {
x, y float64
a Angle
}
// updateTab tells for every possible Kind and Part combination what difference
// it makes to X, Y and Angle to move to the next Part
var updateTab []update
// Update places target part after this one
func (p *Part) Update(target *Part) {
u := &updateTab[int(p.Kind)*AngleN+int(p.Angle)]
target.X = p.X + u.x
target.Y = p.Y + u.y
target.Angle = p.Angle.Add(u.a)
}
func initUpdateTab() {
updateTab = make([]update, int(kindCnt)*AngleN)
// curveLen is the distance between start and end of a curve
n := float64(AngleN)
curveLen := Radius * 2 * math.Sin(math.Pi/n)
for a := 0; a < AngleN; a++ {
s, c := math.Sincos(Angle(a).Rad())
updateTab[int(Bridge)*AngleN+a] = update{
x: c * BridgeLen,
y: s * BridgeLen,
a: 0,
}
updateTab[int(Straight)*AngleN+a] = update{
x: c * StraightLen,
y: s * StraightLen,
a: 0,
}
sl, cl := math.Sincos(Angle(a).Rad() + math.Pi/n)
updateTab[int(Left)*AngleN+a] = update{
x: cl * curveLen,
y: sl * curveLen,
a: 1,
}
sr, cr := math.Sincos(Angle(a).Rad() - math.Pi/n)
updateTab[int(Right)*AngleN+a] = update{
x: cr * curveLen,
y: sr * curveLen,
a: -1,
}
}
}
// findTracks searches the state space of all possible pieces combinations for
// closed tracks that use all pieces. There are some symmetry branch cuts such
// as that the first piece is always the bridge if it present and that the first
// curve always goes to the left.
func findTracks(out chan []Part, bridges, straights, curves int) {
if bridges > 1 {
panic("can handle only 0 or 1 bridges")
}
track := make([]Part, bridges+straights+curves+1)
startAt := 0
if bridges == 1 {
track[0].Kind = Bridge
track[0].Update(&track[1])
startAt = 1
}
n1 := len(track) - 1
curveLen := Radius * 2 * math.Sin(math.Pi/float64(AngleN))
var r func(int, Kind, int, int)
r = func(pos int, kind Kind, s, c int) {
p := &track[pos]
// update next part start
p.Kind = kind
p.Update(&track[pos+1])
// terminate search?
if s == 0 && c == 0 {
if track[0].AtSamePlaceAs(&track[n1]) {
c := make([]Part, n1)
copy(c, track[:n1])
out <- c
}
return
}
// optimization - check that origin is still within reachable distance
p = &track[pos+1]
dist := math.Sqrt(p.X*p.X + p.Y*p.Y)
if dist > float64(s)*StraightLen+float64(c)*curveLen+1 {
// fmt.Println("Cutting on ", pos, dist)
return
}
if s > 0 {
r(pos+1, Straight, s-1, c)
}
if c > 0 {
r(pos+1, Left, s, c-1)
if c < curves { // first curve turns always to the left
r(pos+1, Right, s, c-1)
}
}
}
// start recursion - left only - eliminate symmetrical tracks
r(startAt, Straight, straights-1, curves)
r(startAt, Left, straights, curves-1)
close(out)
}
// simpleFormat formats the the track as letters
func simpleFormat(track []Part) string {
b := bytes.Buffer{}
for i := range track {
b.WriteString(track[i].Kind.String())
}
return b.String()
}
// trackBounds finds the bounding box of the track.
func trackBounds(track []Part) (minX, minY, maxX, maxY float64) {
for i := range track {
p := &track[i]
if minX > p.X {
minX = p.X
}
if maxX < p.X {
maxX = p.X
}
if minY > p.Y {
minY = p.Y
}
if maxY < p.Y {
maxY = p.Y
}
}
return
}
//writeSvg write the track as an SVG image.
func writeSvg(writer io.Writer, track []Part) error {
const unit = 40
const gap = 5
c := svg.New(writer)
minX, minY, maxX, maxY := trackBounds(track)
offX, offY := -minX*unit+gap, -minY*unit+gap
w, h := int((maxX-minX)*unit+2*gap), int((maxY-minY)*unit+2*gap)
c.Start(w, h)
c.ScaleXY(1, -1)
c.Translate(0, -h)
r := int(Radius * float64(unit))
n := float64(AngleN + 1)
r2 := unit * Radius * 2 * math.Sin(math.Pi/n)
x := int(r2 * math.Cos(math.Pi/n))
y := int(r2 * math.Sin(math.Pi/n))
for i := range track {
p := &track[i]
c.TranslateRotate(int(p.X*unit+offX), int(p.Y*unit+offY), p.Angle.Deg())
switch p.Kind {
case Straight:
c.Line(0, 0, int(StraightLen*unit)-gap, 0, "fill:none;stroke:red;stroke-width:5")
case Bridge:
c.Line(0, 0, int(BridgeLen*unit)-gap, 0, "fill:none;stroke:orange;stroke-width:5")
case Left:
c.Arc(0, 0, r, r, 0, false, true, x, y, "fill:none;stroke:green;stroke-width:5")
case Right:
c.Arc(0, 0, r, r, 0, false, false, x, -y, "fill:none;stroke:blue;stroke-width:5")
}
c.Gend() // TranslateRotate
}
c.Gend() //Translate
c.Gend() //Scale
c.End()
return nil
}
// writeSvgFile saves the track as an image to the SVG file with given name.
func writeSvgFile(fn string, track []Part) error {
f, err := os.Create(fn)
if err != nil {
return err
}
defer f.Close()
return writeSvg(f, track)
}
// totalAngle returns the sum of angles in the track. For closed track it will
// always have the form k*AngleN where k is an integer. When it is 0 the track has
// an 8 shape. When it is AngleN the track has an O shape.
func totalAngle(track []Part) (angle int) {
for i := range track {
p := &track[i]
switch p.Kind {
case Left:
angle++
case Right:
angle--
}
}
return
}
// setIkeaParams adusts the part paramters for IKEA LILLABO 20 piece train set
func setIkeaParams() {
AngleN = 8
Radius = 1
BridgeLen = 2
StraightLen = 1
}
func main() {
var (
// lego basic set params
brCnt = flag.Int("b", 1, "number of bridge pieces (may 0 or 1)")
strCnt = flag.Int("s", 5, "number of straight pieces")
curvCnt = flag.Int("c", 20, "number of curve pieces")
ikea = flag.Bool("ikea", false, "toggle piece characteristics to IKEA LILLABO parts (default is LEGO Duplo)")
only8 = flag.Bool("8", false, "output only 8 shaped tracks")
onlyO = flag.Bool("O", false, "output only O shaped tracks")
)
flag.Parse()
if *ikea {
setIkeaParams()
}
initUpdateTab()
ch := make(chan []Part)
go findTracks(ch, *brCnt, *strCnt, *curvCnt)
os.Mkdir("svg", 0755)
o, err := os.Create("all.html")
if err != nil {
log.Fatal("cannot open output file all.html:", err)
}
defer o.Close()
o.Write([]byte("<html><body>"))
i := 0
for tr := range ch {
if *only8 && totalAngle(tr) != 0 || *onlyO && totalAngle(tr) == 0 {
continue
}
fmt.Println(simpleFormat(tr))
fn := fmt.Sprintf("svg/img_%05d.svg", i)
fmt.Fprintf(o, `<img src="%s"><br>`, fn)
writeSvgFile(fn, tr)
i++
}
o.Write([]byte("</body></html>"))
}