@packtrack/layout 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/device/channel.ts +14 -0
- package/device/device.ts +28 -0
- package/district.ts +104 -0
- package/index.ts +399 -0
- package/package.json +4 -0
- package/positioner/index.ts +3 -0
- package/positioner/point.ts +28 -0
- package/positioner/responder-type.ts +9 -0
- package/postion.ts +35 -0
- package/power-district.ts +16 -0
- package/route.ts +16 -0
- package/router.ts +46 -0
- package/section.ts +269 -0
- package/tile.ts +86 -0
- package/track.ts +30 -0
package/device/device.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Channel } from "./channel";
|
|
2
|
+
|
|
3
|
+
export class Device {
|
|
4
|
+
channels: Channel[] = [];
|
|
5
|
+
|
|
6
|
+
lastDiscovery: { date: Date, address: string };
|
|
7
|
+
|
|
8
|
+
constructor(
|
|
9
|
+
public identifier: string
|
|
10
|
+
) {}
|
|
11
|
+
|
|
12
|
+
dump() {
|
|
13
|
+
console.group(`Device ${this.identifier}`);
|
|
14
|
+
|
|
15
|
+
if (this.lastDiscovery) {
|
|
16
|
+
console.log(`last discovery: ${this.lastDiscovery.date.toISOString()} ${this.lastDiscovery.address}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
console.group('channels');
|
|
20
|
+
|
|
21
|
+
for (let channel of this.channels) {
|
|
22
|
+
channel.dump();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.groupEnd();
|
|
26
|
+
console.groupEnd();
|
|
27
|
+
}
|
|
28
|
+
}
|
package/district.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Layout } from ".";
|
|
2
|
+
import { PowerDistrict } from "./power-district";
|
|
3
|
+
import { Router } from "./router";
|
|
4
|
+
import { Section } from "./section";
|
|
5
|
+
|
|
6
|
+
export class District {
|
|
7
|
+
children: District[] = [];
|
|
8
|
+
|
|
9
|
+
powerDistricts: PowerDistrict[] = [];
|
|
10
|
+
sections: Section[] = [];
|
|
11
|
+
routers: Router[] = [];
|
|
12
|
+
|
|
13
|
+
constructor(
|
|
14
|
+
public name: string,
|
|
15
|
+
public parent: District | Layout
|
|
16
|
+
) {}
|
|
17
|
+
|
|
18
|
+
get domainName() {
|
|
19
|
+
if (this.parent instanceof Layout) {
|
|
20
|
+
return `${this.name}.${this.parent.name}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return `${this.name}.${this.parent.domainName}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
dump() {
|
|
27
|
+
console.group(`District ${this.domainName}`);
|
|
28
|
+
|
|
29
|
+
if (this.powerDistricts.length) {
|
|
30
|
+
console.group('power districts');
|
|
31
|
+
|
|
32
|
+
for (let district of this.powerDistricts) {
|
|
33
|
+
district.dump();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.groupEnd();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (this.sections.length) {
|
|
40
|
+
console.group('sections');
|
|
41
|
+
|
|
42
|
+
for (let section of this.sections) {
|
|
43
|
+
section.dump();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.groupEnd();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (this.children.length) {
|
|
50
|
+
console.group('children');
|
|
51
|
+
|
|
52
|
+
for (let district of this.children) {
|
|
53
|
+
district.dump();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
console.groupEnd();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.groupEnd();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
toDotReference() {
|
|
63
|
+
return `cluster_${this.name.replace(/-/g, '_')}${this.parent instanceof District ? this.parent.toDotReference() : ''}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
toDotDefinition() {
|
|
67
|
+
return `
|
|
68
|
+
subgraph ${this.toDotReference()} {
|
|
69
|
+
label = ${JSON.stringify(this.name)}
|
|
70
|
+
|
|
71
|
+
${this.sections.map(section => section.toDotDefinition()).join('')}
|
|
72
|
+
${this.routers.map(router => router.toDotDefinition()).join('')}
|
|
73
|
+
|
|
74
|
+
${this.children.map(child => child.toDotDefinition()).join('')}
|
|
75
|
+
}
|
|
76
|
+
`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
toDotConnection() {
|
|
80
|
+
return `
|
|
81
|
+
${this.sections.map(section => section.toDotConnection()).join('')}
|
|
82
|
+
${this.routers.map(router => router.toDotConnection()).join('')}
|
|
83
|
+
|
|
84
|
+
${this.children.map(child => child.toDotConnection()).join('')}
|
|
85
|
+
`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
toSVG() {
|
|
89
|
+
return `
|
|
90
|
+
<g id=${JSON.stringify(this.domainName)}>
|
|
91
|
+
${this.sections.map(section => section.toSVG()).join('')}
|
|
92
|
+
|
|
93
|
+
${this.children.map(child => child.toSVG()).join('')}
|
|
94
|
+
</g>
|
|
95
|
+
`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
findSVGPositions() {
|
|
99
|
+
return [
|
|
100
|
+
...this.sections.map(section => section.findSVGPositions()),
|
|
101
|
+
...this.children.map(child => child.findSVGPositions())
|
|
102
|
+
];
|
|
103
|
+
}
|
|
104
|
+
}
|
package/index.ts
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import { District } from "./district";
|
|
2
|
+
import { PowerDistrict } from "./power-district";
|
|
3
|
+
import { Route } from "./route";
|
|
4
|
+
import { Router } from "./router";
|
|
5
|
+
import { Section } from "./section";
|
|
6
|
+
import { TilePattern, Tile } from "./tile";
|
|
7
|
+
import { Track } from "./track";
|
|
8
|
+
import { PointPositioner } from "./positioner/point";
|
|
9
|
+
import { Device } from "./device/device";
|
|
10
|
+
import { ResponderType } from "./positioner/responder-type";
|
|
11
|
+
import { Channel } from "./device/channel";
|
|
12
|
+
|
|
13
|
+
export class Layout {
|
|
14
|
+
name: string;
|
|
15
|
+
|
|
16
|
+
districts: District[] = [];
|
|
17
|
+
|
|
18
|
+
devices: Device[] = [];
|
|
19
|
+
responderType: ResponderType[] = [];
|
|
20
|
+
|
|
21
|
+
get allDistricts() {
|
|
22
|
+
const districts: District[] = [];
|
|
23
|
+
|
|
24
|
+
function walkDistrict(district: District) {
|
|
25
|
+
districts.push(district);
|
|
26
|
+
|
|
27
|
+
for (let child of district.children) {
|
|
28
|
+
walkDistrict(child);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
for (let district of this.districts) {
|
|
33
|
+
walkDistrict(district);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return districts;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static from(document: any) {
|
|
40
|
+
const layout = new Layout();
|
|
41
|
+
|
|
42
|
+
const railway = document.firstChild!;
|
|
43
|
+
layout.name = railway.getAttribute('name');
|
|
44
|
+
|
|
45
|
+
const version = railway.getAttribute('version');
|
|
46
|
+
|
|
47
|
+
if (version == '1') {
|
|
48
|
+
let district = railway.firstChild;
|
|
49
|
+
|
|
50
|
+
while (district) {
|
|
51
|
+
if (district.tagName == 'district') {
|
|
52
|
+
layout.districts.push(layout.loadDistrict(district, layout));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
district = district.nextSibling;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
district = railway.firstChild;
|
|
59
|
+
let index = 0;
|
|
60
|
+
|
|
61
|
+
while (district) {
|
|
62
|
+
if (district.tagName == 'district') {
|
|
63
|
+
layout.linkDistrict(district, layout.districts[index]);
|
|
64
|
+
|
|
65
|
+
index++;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
district = district.nextSibling;
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
throw new Error(`unsupported railway definition file version '${version}'`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return layout;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
loadDistrict(source, parent: District | Layout) {
|
|
78
|
+
const district = new District(source.getAttribute('name'), parent);
|
|
79
|
+
|
|
80
|
+
let child = source.firstChild;
|
|
81
|
+
|
|
82
|
+
while (child) {
|
|
83
|
+
if (child.tagName == 'power-districts') {
|
|
84
|
+
let powerDistrict = child.firstChild;
|
|
85
|
+
|
|
86
|
+
while (powerDistrict) {
|
|
87
|
+
if (powerDistrict.tagName == 'power-district') {
|
|
88
|
+
district.powerDistricts.push(this.loadPowerDistrict(powerDistrict, district));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
powerDistrict = powerDistrict.nextSibling;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (child.tagName == 'section') {
|
|
96
|
+
this.loadSection(child, district);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (child.tagName == 'router') {
|
|
100
|
+
district.routers.push(this.loadRouter(child, district));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (child.tagName == 'district') {
|
|
104
|
+
district.children.push(this.loadDistrict(child, district));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
child = child.nextSibling;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return district;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
linkDistrict(source, district: District) {
|
|
114
|
+
let child = source.firstChild;
|
|
115
|
+
|
|
116
|
+
let sectionIndex = 0;
|
|
117
|
+
let childIndex = 0;
|
|
118
|
+
|
|
119
|
+
while (child) {
|
|
120
|
+
if (child.tagName == 'section') {
|
|
121
|
+
this.linkSection(child, district.sections[sectionIndex]);
|
|
122
|
+
|
|
123
|
+
sectionIndex++;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (child.tagName == 'router') {
|
|
127
|
+
this.linkRouter(child, district.routers.find(router => router.name == child.getAttribute('name'))!);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (child.tagName == 'district') {
|
|
131
|
+
this.linkDistrict(child, district.children[childIndex]);
|
|
132
|
+
|
|
133
|
+
childIndex++;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
child = child.nextSibling;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
loadSection(source, district: District) {
|
|
141
|
+
const section = new Section(source.getAttribute('name'), district);
|
|
142
|
+
district.sections.push(section);
|
|
143
|
+
|
|
144
|
+
let child = source.firstChild;
|
|
145
|
+
|
|
146
|
+
while (child) {
|
|
147
|
+
if (child.tagName == 'tracks') {
|
|
148
|
+
let trackNode = child.firstChild;
|
|
149
|
+
|
|
150
|
+
while (trackNode) {
|
|
151
|
+
if (trackNode.tagName == 'track') {
|
|
152
|
+
const track = new Track(
|
|
153
|
+
section,
|
|
154
|
+
+trackNode.getAttribute('length'),
|
|
155
|
+
trackNode.getAttribute('path')
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
section.tracks.push(track);
|
|
159
|
+
|
|
160
|
+
let trackChild = trackNode.firstChild;
|
|
161
|
+
|
|
162
|
+
while (trackChild) {
|
|
163
|
+
if (trackChild.tagName == 'positioners') {
|
|
164
|
+
let positioner = trackChild.firstChild;
|
|
165
|
+
|
|
166
|
+
while (positioner) {
|
|
167
|
+
if (positioner.tagName == 'point') {
|
|
168
|
+
const device = this.findDevice(positioner.getAttribute('device'));
|
|
169
|
+
const channel = this.findChannel(device, positioner.getAttribute('channel'));
|
|
170
|
+
const responderType = this.findResponderType(positioner.getAttribute('responder'));
|
|
171
|
+
|
|
172
|
+
track.positioners.push(new PointPositioner(
|
|
173
|
+
track,
|
|
174
|
+
+positioner.getAttribute('offset'),
|
|
175
|
+
channel,
|
|
176
|
+
responderType
|
|
177
|
+
));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
positioner = positioner.nextSibling;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
trackChild = trackChild.nextSibling;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
trackNode = trackNode.nextSibling;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (child.tagName == 'tile') {
|
|
193
|
+
const pattern = child.getAttribute('pattern');
|
|
194
|
+
|
|
195
|
+
if (!(pattern in TilePattern.patterns)) {
|
|
196
|
+
throw new Error(`Unknown tile pattern '${pattern}' in tile ${section.tiles.length + 1} in ${section.domainName}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
section.tiles.push(new Tile(section, +child.getAttribute('x'), +child.getAttribute('y'), TilePattern.patterns[pattern]))
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
child = child.nextSibling;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
findDevice(identifier: string) {
|
|
207
|
+
let device = this.devices.find(device => device.identifier == identifier);
|
|
208
|
+
|
|
209
|
+
if (device) {
|
|
210
|
+
return device;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
device = new Device(identifier);
|
|
214
|
+
this.devices.push(device);
|
|
215
|
+
|
|
216
|
+
return device;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
findChannel(device: Device, name: string) {
|
|
220
|
+
let channel = device.channels.find(channel => channel.name == name);
|
|
221
|
+
|
|
222
|
+
if (channel) {
|
|
223
|
+
return channel;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
channel = new Channel(device, name);
|
|
227
|
+
device.channels.push(channel);
|
|
228
|
+
|
|
229
|
+
return channel;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
findResponderType(name: string) {
|
|
233
|
+
let type = this.responderType.find(type => type.name == name);
|
|
234
|
+
|
|
235
|
+
if (type) {
|
|
236
|
+
return type;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
type = new ResponderType(name);
|
|
240
|
+
this.responderType.push(type);
|
|
241
|
+
|
|
242
|
+
return type;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
linkSection(source, section: Section) {
|
|
246
|
+
let child = source.firstChild;
|
|
247
|
+
|
|
248
|
+
while (child) {
|
|
249
|
+
if (child.tagName == 'out') {
|
|
250
|
+
const out = this.findSection(child.getAttribute('section'), section.district);
|
|
251
|
+
|
|
252
|
+
section.out = out;
|
|
253
|
+
out.in = section;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
child = child.nextSibling;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
findSection(path: string, base: District, source = base) {
|
|
261
|
+
const parts = path.split('.');
|
|
262
|
+
|
|
263
|
+
if (parts.length == 0) {
|
|
264
|
+
throw `section '${path}' not found from '${source.name}': invalid name`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (parts.length == 1) {
|
|
268
|
+
const localSection = base.sections.find(section => section.name == parts[0]);
|
|
269
|
+
|
|
270
|
+
if (!localSection) {
|
|
271
|
+
throw new Error(`Section '${path}' not found from '${source.name}': section does not exist in '${base.name}'`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return localSection;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const sectionName = parts.pop()!;
|
|
278
|
+
|
|
279
|
+
let pool: District | Layout = base;
|
|
280
|
+
|
|
281
|
+
for (let index = 0; index < parts.length; index++) {
|
|
282
|
+
if (pool instanceof Layout || !pool.parent) {
|
|
283
|
+
throw new Error(`Section '${path}' could not be found from '${source.name}': district '${pool.name}' does not have a parent`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
pool = pool.parent!;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
for (let part of parts) {
|
|
290
|
+
const child = (pool instanceof District ? pool.children : pool.districts).find(child => child.name == part);
|
|
291
|
+
|
|
292
|
+
if (!child) {
|
|
293
|
+
throw new Error(`Section '${path}' could not be found from '${source.name}': district '${pool.name}' does not have a child named '${part}'`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
pool = child;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (pool instanceof Layout) {
|
|
300
|
+
throw new Error(`Section '${path}' could not be found from '${source.name}': a layout cannot directly include a section`);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return this.findSection(sectionName, pool, source);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
loadRouter(source, district: District) {
|
|
307
|
+
const router = new Router(source.getAttribute('name'), district);
|
|
308
|
+
|
|
309
|
+
return router;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
linkRouter(source, router: Router) {
|
|
313
|
+
let child = source.firstChild;
|
|
314
|
+
|
|
315
|
+
while (child) {
|
|
316
|
+
if (child.tagName == 'route') {
|
|
317
|
+
const route = new Route(child.getAttribute('name'), router);
|
|
318
|
+
|
|
319
|
+
route.in = this.findSection(child.getAttribute('in'), router.district);
|
|
320
|
+
route.in.out = router;
|
|
321
|
+
|
|
322
|
+
route.out = this.findSection(child.getAttribute('out'), router.district);
|
|
323
|
+
route.out.in = router;
|
|
324
|
+
|
|
325
|
+
router.routes.push(route);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
child = child.nextSibling;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
loadPowerDistrict(source, district: District) {
|
|
333
|
+
const powerDistrict = new PowerDistrict(source.getAttribute('name'), district);
|
|
334
|
+
|
|
335
|
+
return powerDistrict;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
toDot() {
|
|
339
|
+
let dot = 'digraph G {';
|
|
340
|
+
|
|
341
|
+
for (let district of this.districts) {
|
|
342
|
+
dot += district.toDotDefinition();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
for (let district of this.districts) {
|
|
346
|
+
dot += district.toDotConnection();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return `${dot}}`;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
toSVG(inject = '') {
|
|
353
|
+
const positons = this.districts.map(district => district.findSVGPositions()).flat(Infinity);
|
|
354
|
+
|
|
355
|
+
const width = Math.max(...positons.map(position => position.x));
|
|
356
|
+
const height = Math.max(...positons.map(position => position.y));
|
|
357
|
+
|
|
358
|
+
let svg = `<svg width="100vw" height="100vh" viewBox="0 0 ${width + 1} ${height + 1}" xmlns="http://www.w3.org/2000/svg">
|
|
359
|
+
<style>
|
|
360
|
+
|
|
361
|
+
path {
|
|
362
|
+
fill: none;
|
|
363
|
+
stroke: #000;
|
|
364
|
+
stroke-width: 0.2;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
</style>
|
|
368
|
+
`;
|
|
369
|
+
|
|
370
|
+
for (let district of this.districts) {
|
|
371
|
+
svg += district.toSVG();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return `${svg}${inject}</svg>`;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
dump() {
|
|
378
|
+
console.group(`Layout ${this.name}`);
|
|
379
|
+
console.log('devices');
|
|
380
|
+
|
|
381
|
+
for (let device of this.devices) {
|
|
382
|
+
device.dump();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
console.log('responder types');
|
|
386
|
+
|
|
387
|
+
for (let type of this.responderType) {
|
|
388
|
+
type.dump();
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
console.log('districts');
|
|
392
|
+
|
|
393
|
+
for (let district of this.districts) {
|
|
394
|
+
district.dump();
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
console.groupEnd();
|
|
398
|
+
}
|
|
399
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Positioner } from ".";
|
|
2
|
+
import { Channel } from "../device/channel";
|
|
3
|
+
import { Track } from "../track";
|
|
4
|
+
import { ResponderType } from "./responder-type";
|
|
5
|
+
|
|
6
|
+
export class PointPositioner extends Positioner {
|
|
7
|
+
constructor(
|
|
8
|
+
public track: Track,
|
|
9
|
+
public offset: number,
|
|
10
|
+
public channel: Channel,
|
|
11
|
+
public responder: ResponderType
|
|
12
|
+
) {
|
|
13
|
+
super();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get position() {
|
|
17
|
+
return this.track.head.advance(this.offset);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
dump() {
|
|
21
|
+
console.group('Point positioner');
|
|
22
|
+
console.log('offset:', this.offset);
|
|
23
|
+
|
|
24
|
+
this.channel.dump();
|
|
25
|
+
|
|
26
|
+
console.groupEnd();
|
|
27
|
+
}
|
|
28
|
+
}
|
package/postion.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Section } from '../layout/section.js';
|
|
2
|
+
|
|
3
|
+
export class SectionPosition {
|
|
4
|
+
constructor(
|
|
5
|
+
public section: Section,
|
|
6
|
+
public offset: number,
|
|
7
|
+
public reversed: boolean
|
|
8
|
+
) {}
|
|
9
|
+
|
|
10
|
+
get absolutePosition() {
|
|
11
|
+
if (this.reversed) {
|
|
12
|
+
return this.section.length - this.offset;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return this.offset;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
advance(distance: number) {
|
|
19
|
+
if (this.offset + distance > this.section.length) {
|
|
20
|
+
const next = this.section.next(this.reversed);
|
|
21
|
+
|
|
22
|
+
if (!next) {
|
|
23
|
+
throw new Error(`Illegal advancement ${this} + ${distance}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return new SectionPosition(next, 0, this.reversed).advance(this.offset + distance - this.section.length);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return new SectionPosition(this.section, this.offset + distance, this.reversed);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
toString() {
|
|
33
|
+
return `${this.section.name} @ ${this.offset.toFixed(1)} ${this.reversed ? 'backward' : 'forward'}`;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { District } from "./district";
|
|
2
|
+
|
|
3
|
+
export class PowerDistrict {
|
|
4
|
+
constructor(
|
|
5
|
+
public name: string,
|
|
6
|
+
public district: District
|
|
7
|
+
) {}
|
|
8
|
+
|
|
9
|
+
get domainName() {
|
|
10
|
+
return `${this.name}.${this.district.domainName}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
dump() {
|
|
14
|
+
console.log(this.name);
|
|
15
|
+
}
|
|
16
|
+
}
|
package/route.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Router } from "./router";
|
|
2
|
+
import { Section } from "./section";
|
|
3
|
+
|
|
4
|
+
export class Route {
|
|
5
|
+
in: Section;
|
|
6
|
+
out: Section;
|
|
7
|
+
|
|
8
|
+
constructor(
|
|
9
|
+
public name: string,
|
|
10
|
+
public router: Router,
|
|
11
|
+
) {}
|
|
12
|
+
|
|
13
|
+
dump() {
|
|
14
|
+
console.log(`Route ${this.name}: ${this.in.domainName} → ${this.out.domainName}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
package/router.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { District } from "./district";
|
|
2
|
+
import { Route } from "./route";
|
|
3
|
+
|
|
4
|
+
export class Router {
|
|
5
|
+
activeRoute?: Route;
|
|
6
|
+
|
|
7
|
+
routes: Route[] = [];
|
|
8
|
+
|
|
9
|
+
constructor(
|
|
10
|
+
public name: string,
|
|
11
|
+
public district: District
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
get domainName() {
|
|
15
|
+
return `${this.name}.${this.district.domainName}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
dump() {
|
|
19
|
+
console.group(`Router ${this.domainName}`);
|
|
20
|
+
|
|
21
|
+
for (let route of this.routes) {
|
|
22
|
+
route.dump();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.groupEnd();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
toDotReference() {
|
|
29
|
+
return `router_${this.name.replace(/-/g, '_')}_${this.district.toDotReference()}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
toDotDefinition() {
|
|
33
|
+
return `
|
|
34
|
+
${this.toDotReference()} [ label = ${JSON.stringify(this.name)}, shape = diamond ]
|
|
35
|
+
`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
toDotConnection() {
|
|
39
|
+
return `
|
|
40
|
+
${this.routes.map(route => `
|
|
41
|
+
${route.in.toDotReference()} -> ${this.toDotReference()} [ headlabel = ${JSON.stringify(route.name)} ]
|
|
42
|
+
${this.toDotReference()} -> ${route.out.toDotReference()} [ taillabel = ${JSON.stringify(route.name)} ]
|
|
43
|
+
`).join('')}
|
|
44
|
+
`;
|
|
45
|
+
}
|
|
46
|
+
}
|
package/section.ts
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { District } from "./district";
|
|
2
|
+
import { SectionPosition } from "./postion";
|
|
3
|
+
import { PowerDistrict } from "./power-district";
|
|
4
|
+
import { Router } from "./router";
|
|
5
|
+
import { Tile } from "./tile";
|
|
6
|
+
import { Track } from "./track";
|
|
7
|
+
|
|
8
|
+
export class Section {
|
|
9
|
+
powerDistrict: PowerDistrict;
|
|
10
|
+
|
|
11
|
+
tracks: Track[] = [];
|
|
12
|
+
tiles: Tile[] = [];
|
|
13
|
+
|
|
14
|
+
in?: Router | Section;
|
|
15
|
+
out?: Router | Section;
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
public name: string,
|
|
19
|
+
public district: District
|
|
20
|
+
) {}
|
|
21
|
+
|
|
22
|
+
get domainName() {
|
|
23
|
+
return `${this.name}.${this.district.domainName}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// returns the next currently set section
|
|
27
|
+
next(reverse: boolean) {
|
|
28
|
+
if (reverse) {
|
|
29
|
+
if (!this.in) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (this.in instanceof Section) {
|
|
34
|
+
return this.in;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (this.in instanceof Router) {
|
|
38
|
+
const activeRoute = this.in.activeRoute;
|
|
39
|
+
|
|
40
|
+
if (!activeRoute) {
|
|
41
|
+
throw new Error(`Router '${this.in.domainName}' has no active route`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (activeRoute.in == this) {
|
|
45
|
+
return activeRoute.out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (activeRoute.out == this) {
|
|
49
|
+
return activeRoute.in;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
throw new Error(`Router '${this.in.domainName}' is not routing from '${this.domainName}'`);
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
if (!this.out) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (this.out instanceof Section) {
|
|
60
|
+
return this.out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (this.out instanceof Router) {
|
|
64
|
+
const activeRoute = this.out.activeRoute;
|
|
65
|
+
|
|
66
|
+
if (!activeRoute) {
|
|
67
|
+
throw new Error(`Router '${this.out.domainName}' has no active route`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (activeRoute.in == this) {
|
|
71
|
+
return activeRoute.out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (activeRoute.out == this) {
|
|
75
|
+
return activeRoute.in;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
throw new Error(`Router '${this.in?.domainName}' is not routing from '${this.domainName}'`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
getTilesInRange(startPosition: SectionPosition, endPosition: SectionPosition) {
|
|
84
|
+
if (startPosition.section != this && endPosition.section != this) {
|
|
85
|
+
return {
|
|
86
|
+
offset: {
|
|
87
|
+
start: 0,
|
|
88
|
+
end: this.tiles[this.tiles.length - 1].pattern.length
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
tiles: [...this.tiles]
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let start = 0;
|
|
96
|
+
let end = this.length;
|
|
97
|
+
|
|
98
|
+
// only use the position limit if it is within our section
|
|
99
|
+
if (startPosition.section == this) {
|
|
100
|
+
end = startPosition.absolutePosition;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (endPosition.section == this) {
|
|
104
|
+
start = endPosition.absolutePosition;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// flip if the range was reversed
|
|
108
|
+
if (end < start) {
|
|
109
|
+
const small = end;
|
|
110
|
+
|
|
111
|
+
end = start;
|
|
112
|
+
start = small;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let passed = 0;
|
|
116
|
+
const tiles: Tile[] = [];
|
|
117
|
+
|
|
118
|
+
const tileUnitLength = this.length / this.tileLength;
|
|
119
|
+
|
|
120
|
+
const offset = {
|
|
121
|
+
start: 0,
|
|
122
|
+
end: 0
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
for (let tile of this.tiles) {
|
|
126
|
+
const length = tile.pattern.length * tileUnitLength;
|
|
127
|
+
|
|
128
|
+
if (start - length <= passed && end + length >= passed) {
|
|
129
|
+
tiles.push(tile);
|
|
130
|
+
|
|
131
|
+
if (start <= passed) {
|
|
132
|
+
offset.start = (start + length - passed) * tile.pattern.length / length;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (end >= passed) {
|
|
136
|
+
offset.end = 0.5; // (start + length - passed) * tile.pattern.length / length;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
passed += length;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
offset,
|
|
145
|
+
tiles
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
dump() {
|
|
150
|
+
console.group(`Section ${this.domainName}`);
|
|
151
|
+
|
|
152
|
+
console.log('in', this.in?.name ?? 'buffer');
|
|
153
|
+
console.log('out', this.out?.name ?? 'buffer');
|
|
154
|
+
|
|
155
|
+
console.group(`tracks`);
|
|
156
|
+
|
|
157
|
+
for (let track of this.tracks) {
|
|
158
|
+
track.dump();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
console.groupEnd();
|
|
162
|
+
console.groupEnd();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
get length() {
|
|
166
|
+
return this.tracks.reduce((accumulator, track) => accumulator + track.length, 0);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
get tileLength() {
|
|
170
|
+
return this.tiles.reduce((accumulator, tile) => accumulator + tile.pattern.length, 0);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// follow the active path ahead (reverse = true = back)
|
|
174
|
+
// uses the currently set routes
|
|
175
|
+
// returns all touched sections
|
|
176
|
+
trail(offset: number, reversed: boolean, length: number) {
|
|
177
|
+
let tip: Section = this;
|
|
178
|
+
const sections: Section[] = [this];
|
|
179
|
+
|
|
180
|
+
if (reversed) {
|
|
181
|
+
length -= offset;
|
|
182
|
+
} else {
|
|
183
|
+
length -= this.length - offset;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
while (length > 0) {
|
|
187
|
+
let next: Router | Section | undefined;
|
|
188
|
+
|
|
189
|
+
if (reversed) {
|
|
190
|
+
next = tip.in;
|
|
191
|
+
} else {
|
|
192
|
+
next = tip.out;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!next) {
|
|
196
|
+
return {
|
|
197
|
+
sections,
|
|
198
|
+
|
|
199
|
+
tip: new SectionPosition(tip, tip.length, false)
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (next instanceof Section) {
|
|
204
|
+
tip = next;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (next instanceof Router) {
|
|
208
|
+
if (!next.activeRoute) {
|
|
209
|
+
throw new Error(`Router '${next.domainName}' has no active route (routes: ${next.routes.map(route => `'${route.name}'`).join(', ')})`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// TODO: handle flipped cases
|
|
213
|
+
if (reversed) {
|
|
214
|
+
tip = next.activeRoute!.in;
|
|
215
|
+
} else {
|
|
216
|
+
tip = next.activeRoute!.out;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
sections.push(tip);
|
|
221
|
+
length -= tip.length;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
sections,
|
|
226
|
+
|
|
227
|
+
tip: new SectionPosition(tip, reversed ? -length : tip.length + length, false)
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
toDotReference() {
|
|
232
|
+
return `section_${this.name.replace(/-/g, '_')}_${this.district.toDotReference()}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
toDotDefinition() {
|
|
236
|
+
return `
|
|
237
|
+
${this.toDotReference()} [ label = ${JSON.stringify(`${this.name}\n${this.length}`)}, shape = box ]
|
|
238
|
+
`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
toDotConnection() {
|
|
242
|
+
return `
|
|
243
|
+
${this.out instanceof Section ? `${this.toDotReference()} -> ${this.out.toDotReference()}` : ''}
|
|
244
|
+
`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
toSVG() {
|
|
248
|
+
return `
|
|
249
|
+
<g id=${JSON.stringify(this.domainName).split('.').join('_')}>
|
|
250
|
+
<style>
|
|
251
|
+
|
|
252
|
+
g#${this.domainName.split('.').join('_')} path {
|
|
253
|
+
stroke: hsl(${(this.length / this.tileLength)}deg, 100%, 50%);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
</style>
|
|
257
|
+
|
|
258
|
+
${this.tiles.map(tile => tile.toSVG()).join('')}
|
|
259
|
+
</g>
|
|
260
|
+
`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
findSVGPositions() {
|
|
264
|
+
return this.tiles.map(tile => ({
|
|
265
|
+
x: tile.x,
|
|
266
|
+
y: tile.y
|
|
267
|
+
}));
|
|
268
|
+
}
|
|
269
|
+
}
|
package/tile.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Section } from './section.js';
|
|
2
|
+
|
|
3
|
+
export class TilePattern {
|
|
4
|
+
// TODO: replace simple paths with rounded versions
|
|
5
|
+
static patterns = {
|
|
6
|
+
'tl-cc': new TilePattern(0.7, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 0} ${y + 0}, L ${x + 0.5} ${y + 0.5}`),
|
|
7
|
+
'tc-cc': new TilePattern(0.5, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 0.5} ${y + 0}, L ${x + 0.5} ${y + 0.5}`),
|
|
8
|
+
'tr-cc': new TilePattern(0.7, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 1} ${y + 0}, L ${x + 0.5} ${y + 0.5}`),
|
|
9
|
+
'cl-cc': new TilePattern(0.5, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 0} ${y + 0.5}, L ${x + 0.5} ${y + 0.5}`),
|
|
10
|
+
'cr-cc': new TilePattern(0.5, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 1} ${y + 0.5}, L ${x + 0.5} ${y + 0.5}`),
|
|
11
|
+
'bl-cc': new TilePattern(0.7, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 0} ${y + 1}, L ${x + 0.5} ${y + 0.5}`),
|
|
12
|
+
'bc-cc': new TilePattern(0.5, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 0.5} ${y + 1}, L ${x + 0.5} ${y + 0.5}`),
|
|
13
|
+
'br-cc': new TilePattern(0.7, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 1} ${y + 1}, L ${x + 0.5} ${y + 0.5}`),
|
|
14
|
+
|
|
15
|
+
'cc-tl': new TilePattern(0.7, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 0.5} ${y + 0.5}, L ${x + 0} ${y + 0}`),
|
|
16
|
+
'cc-tc': new TilePattern(0.5, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 0.5} ${y + 0.5}, L ${x + 0.5} ${y + 0}`),
|
|
17
|
+
'cc-tr': new TilePattern(0.7, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 0.5} ${y + 0.5}, L ${x + 1} ${y + 0}`),
|
|
18
|
+
'cc-cl': new TilePattern(0.5, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 0.5} ${y + 0.5}, L ${x + 0} ${y + 0.5}`),
|
|
19
|
+
'cc-cr': new TilePattern(0.5, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 0.5} ${y + 0.5}, L ${x + 1} ${y + 0.5}`),
|
|
20
|
+
'cc-bl': new TilePattern(0.7, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 0.5} ${y + 0.5}, L ${x + 0} ${y + 1}`),
|
|
21
|
+
'cc-bc': new TilePattern(0.5, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 0.5} ${y + 0.5}, L ${x + 0.5} ${y + 1}`),
|
|
22
|
+
'cc-br': new TilePattern(0.7, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 0.5} ${y + 0.5}, L ${x + 1} ${y + 1}`),
|
|
23
|
+
|
|
24
|
+
'cr-cl': new TilePattern(1, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 1} ${y + 0.5}, L ${x + 0} ${y + 0.5}`),
|
|
25
|
+
'cr-tl': new TilePattern(1.2, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 1} ${y + 0.5}, L ${x + 0.5} ${y + 0.5}, L ${x + 0} ${y + 0}`),
|
|
26
|
+
'cr-bl': new TilePattern(1.2, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 1} ${y + 0.5}, L ${x + 0.5} ${y + 0.5}, L ${x + 0} ${y + 1}`),
|
|
27
|
+
|
|
28
|
+
'cl-cr': new TilePattern(1, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 0} ${y + 0.5}, L ${x + 1} ${y + 0.5}`),
|
|
29
|
+
'cl-tr': new TilePattern(1.2, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 0} ${y + 0.5}, L ${x + 0.5} ${y + 0.5}, L ${x + 1} ${y + 0}`),
|
|
30
|
+
'cl-bl': new TilePattern(1.2, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 0} ${y + 0.5}, L ${x + 0.5} ${y + 0.5}, L ${x + 1} ${y + 1}`),
|
|
31
|
+
|
|
32
|
+
'tl-bc': new TilePattern(1.2, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 0} ${y + 0}, L ${x + 0.5} ${y + 0.5}, L ${x + 0.5} ${y + 1}`),
|
|
33
|
+
'tl-br': new TilePattern(1.4, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 0} ${y + 0}, L ${x + 0.5} ${y + 0.5}, L ${x + 1} ${y + 1}`),
|
|
34
|
+
'tl-cr': new TilePattern(1.2, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 0} ${y + 0}, L ${x + 0.5} ${y + 0.5}, L ${x + 1} ${y + 0.5}`),
|
|
35
|
+
|
|
36
|
+
'tc-bc': new TilePattern(1, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 0.5} ${y + 0}, L ${x + 0.5} ${y + 1}`),
|
|
37
|
+
'tc-br': new TilePattern(1.2, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 0.5} ${y + 0}, L ${x + 0.5} ${y + 0.5}, L ${x + 1} ${y + 1}`),
|
|
38
|
+
'tc-bl': new TilePattern(1.2, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 0.5} ${y + 0}, L ${x + 0.5} ${y + 0.5}, L ${x + 0} ${y + 1}`),
|
|
39
|
+
|
|
40
|
+
'tr-cl': new TilePattern(1.2, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 1} ${y + 0}, L ${x + 0.5} ${y + 0.5}, L ${x + 0} ${y + 0.5}`),
|
|
41
|
+
'tr-bc': new TilePattern(1.2, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 1} ${y + 0}, L ${x + 0.5} ${y + 0.5}, L ${x + 0.5} ${y + 1}`),
|
|
42
|
+
|
|
43
|
+
'bl-tr': new TilePattern(1.4, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 0} ${y + 1}, L ${x + 0.5} ${y + 0.5}, L ${x + 1} ${y + 0}`),
|
|
44
|
+
'bl-tc': new TilePattern(1.2, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 0} ${y + 1}, L ${x + 0.5} ${y + 0.5}, L ${x + 0.5} ${y + 0}`),
|
|
45
|
+
'bl-cr': new TilePattern(1.2, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 0} ${y + 1}, L ${x + 0.5} ${y + 0.5}, L ${x + 1} ${y + 0.5}`),
|
|
46
|
+
|
|
47
|
+
'bc-tc': new TilePattern(1, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 0.5} ${y + 1}, L ${x + 0.5} ${y + 0.5}, L ${x + 0.5} ${y + 0}`),
|
|
48
|
+
'bc-tr': new TilePattern(1.2, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 0.5} ${y + 1}, L ${x + 0.5} ${y + 0.5}, L ${x + 1} ${y + 0}`),
|
|
49
|
+
'bc-tl': new TilePattern(1.2, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 0.5} ${y + 1}, L ${x + 0.5} ${y + 0.5}, L ${x + 0} ${y + 0}`),
|
|
50
|
+
|
|
51
|
+
'br-cl': new TilePattern(1.2, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 1} ${y + 1}, L ${x + 0.5} ${y + 0.5}, L ${x + 0} ${y + 0.5}`),
|
|
52
|
+
'br-tc': new TilePattern(1.2, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 1} ${y + 1}, L ${x + 0.5} ${y + 0.5}, L ${x + 0.5} ${y + 0}`),
|
|
53
|
+
|
|
54
|
+
'br-tl': new TilePattern(1.4, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 1} ${y + 1}, L ${x + 0.5} ${y + 0.5}, L ${x + 0} ${y + 0}`),
|
|
55
|
+
|
|
56
|
+
// oddities
|
|
57
|
+
'cl-bc': new TilePattern(0.7, (continuous, x, y) => `${continuous ? 'L' : 'M'} ${x + 0} ${y + 0.5}, L ${x + 0.5} ${y + 1}`)
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
constructor(
|
|
61
|
+
public length: number,
|
|
62
|
+
public path: (continuous: boolean, x: number, y: number) => string
|
|
63
|
+
) {}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class Tile {
|
|
67
|
+
constructor(
|
|
68
|
+
public section: Section,
|
|
69
|
+
|
|
70
|
+
public x: number,
|
|
71
|
+
public y: number,
|
|
72
|
+
|
|
73
|
+
public pattern: TilePattern
|
|
74
|
+
) {}
|
|
75
|
+
|
|
76
|
+
toSVGPath(continuous = false) {
|
|
77
|
+
return this.pattern.path(continuous, this.x, this.y);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
toSVG() {
|
|
81
|
+
return `
|
|
82
|
+
<path d="${this.toSVGPath()}" />
|
|
83
|
+
${this.section.tiles[0] == this ? `<text x="${this.x}" y="${this.y}" font-size="0.2">${this.section.name}</text>` : ''}
|
|
84
|
+
`;
|
|
85
|
+
}
|
|
86
|
+
}
|
package/track.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Positioner } from "./positioner";
|
|
2
|
+
import { Section } from "./section";
|
|
3
|
+
import { SectionPosition } from "./postion";
|
|
4
|
+
|
|
5
|
+
export class Track {
|
|
6
|
+
positioners: Positioner[] = [];
|
|
7
|
+
|
|
8
|
+
constructor(
|
|
9
|
+
public section: Section,
|
|
10
|
+
public length: number,
|
|
11
|
+
public path: string
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
// the start of the track within the section
|
|
15
|
+
get head() {
|
|
16
|
+
let offset = 0;
|
|
17
|
+
|
|
18
|
+
for (let track of this.section.tracks) {
|
|
19
|
+
if (track == this) {
|
|
20
|
+
return new SectionPosition(this.section, offset, false);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
offset += track.length;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
dump() {
|
|
28
|
+
console.log(this.length);
|
|
29
|
+
}
|
|
30
|
+
}
|