pth_to_lyt 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/.prettierrc.json +6 -0
- package/dist/core/PTHToLYT.d.ts +1 -0
- package/dist/core/PTHToLYT.js +15 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +12 -0
- package/dist/input/PTH.parser.d.ts +2 -0
- package/dist/input/PTH.parser.js +74 -0
- package/dist/input/PTH.reader.d.ts +1 -0
- package/dist/input/PTH.reader.js +14 -0
- package/dist/input/types.d.ts +28 -0
- package/dist/input/types.js +2 -0
- package/dist/output/LYT.transform.d.ts +8 -0
- package/dist/output/LYT.transform.js +85 -0
- package/dist/output/LYT.writer.d.ts +2 -0
- package/dist/output/LYT.writer.js +44 -0
- package/dist/output/constants.d.ts +3 -0
- package/dist/output/constants.js +6 -0
- package/dist/output/types.d.ts +16 -0
- package/dist/output/types.js +2 -0
- package/package.json +17 -0
- package/src/core/PTHToLYT.ts +24 -0
- package/src/index.ts +9 -0
- package/src/input/PTH.parser.ts +86 -0
- package/src/input/PTH.reader.ts +10 -0
- package/src/input/types.ts +31 -0
- package/src/output/LYT.transform.ts +114 -0
- package/src/output/LYT.writer.ts +51 -0
- package/src/output/constants.ts +3 -0
- package/src/output/types.ts +18 -0
- package/tsconfig.json +21 -0
package/.prettierrc.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function convertPTHtoLYT(pthFolderPath: string, lytFolderPath: string, trackPrefix: string, lytName: string): void;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.convertPTHtoLYT = convertPTHtoLYT;
|
|
4
|
+
const PTH_parser_1 = require("input/PTH.parser");
|
|
5
|
+
const PTH_reader_1 = require("input/PTH.reader");
|
|
6
|
+
const LYT_transform_1 = require("output/LYT.transform");
|
|
7
|
+
const LYT_writer_1 = require("output/LYT.writer");
|
|
8
|
+
function convertPTHtoLYT(pthFolderPath, lytFolderPath, trackPrefix, lytName) {
|
|
9
|
+
const pthPath = `${pthFolderPath}/${trackPrefix}.pth`;
|
|
10
|
+
const lytPath = `${lytFolderPath}/${trackPrefix}_${lytName}.lyt`;
|
|
11
|
+
const raw = (0, PTH_reader_1.readPTH)(pthPath);
|
|
12
|
+
const data = (0, PTH_parser_1.parserPTH)(raw);
|
|
13
|
+
const objectArray = (0, LYT_transform_1.transformLYT)(data.mainNodes, data.numberNodes);
|
|
14
|
+
(0, LYT_writer_1.writeLYT)(lytPath, objectArray);
|
|
15
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.transformLYTObjectPosition = exports.transformLYT = exports.calculateMidPoint = exports.calculateHeading = exports.calculateHalfWidth = exports.calculateDriveLimits = exports.convertPTHtoLYT = void 0;
|
|
4
|
+
var PTHToLYT_1 = require("./core/PTHToLYT");
|
|
5
|
+
Object.defineProperty(exports, "convertPTHtoLYT", { enumerable: true, get: function () { return PTHToLYT_1.convertPTHtoLYT; } });
|
|
6
|
+
var LYT_transform_1 = require("./output/LYT.transform");
|
|
7
|
+
Object.defineProperty(exports, "calculateDriveLimits", { enumerable: true, get: function () { return LYT_transform_1.calculateDriveLimits; } });
|
|
8
|
+
Object.defineProperty(exports, "calculateHalfWidth", { enumerable: true, get: function () { return LYT_transform_1.calculateHalfWidth; } });
|
|
9
|
+
Object.defineProperty(exports, "calculateHeading", { enumerable: true, get: function () { return LYT_transform_1.calculateHeading; } });
|
|
10
|
+
Object.defineProperty(exports, "calculateMidPoint", { enumerable: true, get: function () { return LYT_transform_1.calculateMidPoint; } });
|
|
11
|
+
Object.defineProperty(exports, "transformLYT", { enumerable: true, get: function () { return LYT_transform_1.transformLYT; } });
|
|
12
|
+
Object.defineProperty(exports, "transformLYTObjectPosition", { enumerable: true, get: function () { return LYT_transform_1.transformLYTObjectPosition; } });
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parserPTH = parserPTH;
|
|
4
|
+
function parserPTH(buffer) {
|
|
5
|
+
const systemHeader = readSystemHeader(buffer);
|
|
6
|
+
const localHeader = readLocalHeader(buffer);
|
|
7
|
+
const numberNodes = readNumberNodes(buffer);
|
|
8
|
+
const mainNodes = readMainNodes(buffer, numberNodes);
|
|
9
|
+
return {
|
|
10
|
+
systemHeader,
|
|
11
|
+
localHeader,
|
|
12
|
+
numberNodes,
|
|
13
|
+
mainNodes,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function readSystemHeader(buffer) {
|
|
17
|
+
let offset = 0;
|
|
18
|
+
const magic = buffer.toString("ascii", offset, offset + 6);
|
|
19
|
+
offset += 6;
|
|
20
|
+
if (magic !== "SRPATH") {
|
|
21
|
+
throw new Error("Invalid File (SRPATH not found)");
|
|
22
|
+
}
|
|
23
|
+
const version = buffer.readUInt8(offset++);
|
|
24
|
+
const revision = buffer.readUInt8(offset++);
|
|
25
|
+
if (version > 0 || revision > 252) {
|
|
26
|
+
throw new Error("Wrong version");
|
|
27
|
+
}
|
|
28
|
+
const flags = buffer.readInt32LE(offset);
|
|
29
|
+
return {
|
|
30
|
+
version,
|
|
31
|
+
revision,
|
|
32
|
+
flags,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function readLocalHeader(buffer) {
|
|
36
|
+
let offset = 12;
|
|
37
|
+
const miniRev = buffer.readUInt8(offset++);
|
|
38
|
+
return {
|
|
39
|
+
miniRev,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function readNumberNodes(buffer) {
|
|
43
|
+
let offset = 16;
|
|
44
|
+
const numberNodes = buffer.readUInt16LE(offset);
|
|
45
|
+
if (numberNodes <= 0)
|
|
46
|
+
throw new Error("The number of nodes is too low");
|
|
47
|
+
return numberNodes;
|
|
48
|
+
}
|
|
49
|
+
function readNode(offset, buffer) {
|
|
50
|
+
const node = {
|
|
51
|
+
centre: {
|
|
52
|
+
x: buffer.readInt32LE(offset + 4),
|
|
53
|
+
y: buffer.readInt32LE(offset + 8),
|
|
54
|
+
},
|
|
55
|
+
dir: {
|
|
56
|
+
x: buffer.readFloatLE(offset + 16),
|
|
57
|
+
y: buffer.readFloatLE(offset + 20),
|
|
58
|
+
},
|
|
59
|
+
limits: {
|
|
60
|
+
driveLeft: buffer.readFloatLE(offset + 36),
|
|
61
|
+
driveRight: buffer.readFloatLE(offset + 40),
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
return node;
|
|
65
|
+
}
|
|
66
|
+
function readMainNodes(buffer, length) {
|
|
67
|
+
const startOffset = 56;
|
|
68
|
+
const nodes = [];
|
|
69
|
+
for (let i = 0; i < length; i++) {
|
|
70
|
+
const offset = startOffset + 44 * i;
|
|
71
|
+
nodes[i] = readNode(offset, buffer);
|
|
72
|
+
}
|
|
73
|
+
return nodes;
|
|
74
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function readPTH(path: string): Buffer<ArrayBuffer>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.readPTH = readPTH;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
function readPTH(path) {
|
|
9
|
+
const buffer = fs_1.default.readFileSync(path);
|
|
10
|
+
if (buffer === undefined) {
|
|
11
|
+
throw new Error("Invalid File");
|
|
12
|
+
}
|
|
13
|
+
return buffer;
|
|
14
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type PTH = {
|
|
2
|
+
systemHeader: SystemHeader;
|
|
3
|
+
localHeader: LocalHeader;
|
|
4
|
+
numberNodes: number;
|
|
5
|
+
mainNodes: PTHNode[];
|
|
6
|
+
};
|
|
7
|
+
export type SystemHeader = {
|
|
8
|
+
version: number;
|
|
9
|
+
revision: number;
|
|
10
|
+
flags: number;
|
|
11
|
+
};
|
|
12
|
+
export type LocalHeader = {
|
|
13
|
+
miniRev: number;
|
|
14
|
+
};
|
|
15
|
+
export type PTHNode = {
|
|
16
|
+
centre: {
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
};
|
|
20
|
+
dir: {
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
};
|
|
24
|
+
limits: {
|
|
25
|
+
driveLeft: number;
|
|
26
|
+
driveRight: number;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { PTHNode } from "input/types";
|
|
2
|
+
import { DriveLimits, LYTObject, Vector2 } from "./types";
|
|
3
|
+
export declare function transformLYT(nodes: PTHNode[], numberNodes: number): LYTObject[];
|
|
4
|
+
export declare function transformLYTObjectPosition(value: number): number;
|
|
5
|
+
export declare function calculateHeading(dirX: number, dirY: number): number;
|
|
6
|
+
export declare function calculateDriveLimits(node: PTHNode): DriveLimits;
|
|
7
|
+
export declare function calculateMidPoint(driveLimits: DriveLimits): Vector2;
|
|
8
|
+
export declare function calculateHalfWidth(driveLimits: DriveLimits, widthOffset?: number): number;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.transformLYT = transformLYT;
|
|
4
|
+
exports.transformLYTObjectPosition = transformLYTObjectPosition;
|
|
5
|
+
exports.calculateHeading = calculateHeading;
|
|
6
|
+
exports.calculateDriveLimits = calculateDriveLimits;
|
|
7
|
+
exports.calculateMidPoint = calculateMidPoint;
|
|
8
|
+
exports.calculateHalfWidth = calculateHalfWidth;
|
|
9
|
+
const constants_1 = require("./constants");
|
|
10
|
+
// LFS NOTATION
|
|
11
|
+
// 0 = world y axis direction
|
|
12
|
+
// LYT NOTATION
|
|
13
|
+
// 128 : heading of zero
|
|
14
|
+
// 192 : heading of 90 degrees
|
|
15
|
+
// 0 : heading of 180 degrees
|
|
16
|
+
// 64 : heading of -90 degrees
|
|
17
|
+
function transformLYT(nodes, numberNodes) {
|
|
18
|
+
const ZBYTE = 240;
|
|
19
|
+
const CHECKPOINT_INDEX = 252;
|
|
20
|
+
const FIRST_CHECKPOINT_FLAG = 0x01;
|
|
21
|
+
const MAX_NODE = 180;
|
|
22
|
+
const objectArray = [];
|
|
23
|
+
const length = numberNodes < MAX_NODE ? numberNodes : MAX_NODE;
|
|
24
|
+
const gap = numberNodes / length;
|
|
25
|
+
for (let i = 0; i < length; i++) {
|
|
26
|
+
const nodeIndex = Math.round(gap * i);
|
|
27
|
+
const node = nodes[nodeIndex];
|
|
28
|
+
const heading = calculateHeading(node.dir.x, node.dir.y);
|
|
29
|
+
const driveLimits = calculateDriveLimits(node);
|
|
30
|
+
const halfWidth = calculateHalfWidth(driveLimits);
|
|
31
|
+
const midPoint = calculateMidPoint(driveLimits);
|
|
32
|
+
const lytObjectX = transformLYTObjectPosition(midPoint.x);
|
|
33
|
+
const lytObjectY = transformLYTObjectPosition(midPoint.y);
|
|
34
|
+
objectArray.push({
|
|
35
|
+
x: lytObjectX,
|
|
36
|
+
y: lytObjectY,
|
|
37
|
+
z: ZBYTE,
|
|
38
|
+
flags: (halfWidth << 2) | FIRST_CHECKPOINT_FLAG,
|
|
39
|
+
index: CHECKPOINT_INDEX,
|
|
40
|
+
heading: heading,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return objectArray;
|
|
44
|
+
}
|
|
45
|
+
function transformLYTObjectPosition(value) {
|
|
46
|
+
return Math.round(value / constants_1.SCALAR_FACTOR);
|
|
47
|
+
}
|
|
48
|
+
function calculateHeading(dirX, dirY) {
|
|
49
|
+
const angleRadians = Math.atan2(dirY, dirX);
|
|
50
|
+
const angleDegrees = angleRadians * (180 / Math.PI);
|
|
51
|
+
//"Heading represents 360 degrees in 256 values."
|
|
52
|
+
const heading = Math.round(((angleDegrees - 90 + 180) * 256) / 360) & 0xff;
|
|
53
|
+
return heading;
|
|
54
|
+
}
|
|
55
|
+
function calculateDriveLimits(node) {
|
|
56
|
+
// "A node is represented by a line perpendicular to its direction."
|
|
57
|
+
// perpendicular = 90 degrees
|
|
58
|
+
// rotate 90 degrees to left for find drive limits direct
|
|
59
|
+
const perpendicularX = -node.dir.y;
|
|
60
|
+
const perpendicularY = node.dir.x;
|
|
61
|
+
// driveLeft negative
|
|
62
|
+
// driveRight positive
|
|
63
|
+
const left = {
|
|
64
|
+
x: node.centre.x - perpendicularX * node.limits.driveLeft * constants_1.LFS_METER,
|
|
65
|
+
y: node.centre.y - perpendicularY * node.limits.driveLeft * constants_1.LFS_METER,
|
|
66
|
+
};
|
|
67
|
+
const right = {
|
|
68
|
+
x: node.centre.x - perpendicularX * node.limits.driveRight * constants_1.LFS_METER,
|
|
69
|
+
y: node.centre.y - perpendicularY * node.limits.driveRight * constants_1.LFS_METER,
|
|
70
|
+
};
|
|
71
|
+
return { left, right };
|
|
72
|
+
}
|
|
73
|
+
function calculateMidPoint(driveLimits) {
|
|
74
|
+
const x = (driveLimits.right.x + driveLimits.left.x) / 2;
|
|
75
|
+
const y = (driveLimits.right.y + driveLimits.left.y) / 2;
|
|
76
|
+
return { x, y };
|
|
77
|
+
}
|
|
78
|
+
// "half width in metres (1 to 31 ...)."
|
|
79
|
+
function calculateHalfWidth(driveLimits, widthOffset = 2) {
|
|
80
|
+
const distanceX = Math.pow(driveLimits.right.x - driveLimits.left.x, 2);
|
|
81
|
+
const distanceY = Math.pow(driveLimits.right.y - driveLimits.left.y, 2);
|
|
82
|
+
const distance = Math.sqrt(distanceX + distanceY);
|
|
83
|
+
const halfWidth = Math.round(distance / constants_1.LFS_METER / 2 + widthOffset);
|
|
84
|
+
return halfWidth > 31 ? 31 : halfWidth;
|
|
85
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.writeLYT = writeLYT;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
function writeLYT(path, objectArray) {
|
|
9
|
+
const bufferHeader = buildBufferHeader(objectArray.length);
|
|
10
|
+
const bufferObjects = buildBufferLYTObject(objectArray);
|
|
11
|
+
const bufferList = [bufferHeader, bufferObjects];
|
|
12
|
+
const buffer = Buffer.concat(bufferList);
|
|
13
|
+
fs_1.default.writeFileSync(path, buffer);
|
|
14
|
+
}
|
|
15
|
+
function buildBufferHeader(length) {
|
|
16
|
+
const buffer = Buffer.alloc(12);
|
|
17
|
+
buffer.write("LFSLYT", 0, "ascii");
|
|
18
|
+
buffer.writeUInt8(0, 6);
|
|
19
|
+
buffer.writeUInt8(252, 7);
|
|
20
|
+
buffer.writeUInt16LE(length, 8);
|
|
21
|
+
buffer.writeUInt8(0, 10);
|
|
22
|
+
buffer.writeUInt8(9, 11);
|
|
23
|
+
return buffer;
|
|
24
|
+
}
|
|
25
|
+
function buildBufferLYTObject(objectArray) {
|
|
26
|
+
const buffer = Buffer.alloc(8 * objectArray.length);
|
|
27
|
+
let offset = 0;
|
|
28
|
+
for (let i = 0; i < objectArray.length; i++) {
|
|
29
|
+
const object = objectArray[i];
|
|
30
|
+
buffer.writeInt16LE(object.x, offset);
|
|
31
|
+
offset += 2;
|
|
32
|
+
buffer.writeInt16LE(object.y, offset);
|
|
33
|
+
offset += 2;
|
|
34
|
+
buffer.writeUInt8(object.z, offset);
|
|
35
|
+
offset++;
|
|
36
|
+
buffer.writeUInt8(object.flags, offset);
|
|
37
|
+
offset++;
|
|
38
|
+
buffer.writeUInt8(object.index, offset);
|
|
39
|
+
offset++;
|
|
40
|
+
buffer.writeUInt8(object.heading, offset);
|
|
41
|
+
offset++;
|
|
42
|
+
}
|
|
43
|
+
return buffer;
|
|
44
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SCALAR_FACTOR = exports.LYT_METER = exports.LFS_METER = void 0;
|
|
4
|
+
exports.LFS_METER = 65536; // 1m
|
|
5
|
+
exports.LYT_METER = 16; // 1m
|
|
6
|
+
exports.SCALAR_FACTOR = exports.LFS_METER / exports.LYT_METER;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type LYTObject = {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
z: number;
|
|
5
|
+
flags: number;
|
|
6
|
+
index: number;
|
|
7
|
+
heading: number;
|
|
8
|
+
};
|
|
9
|
+
export type Vector2 = {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
};
|
|
13
|
+
export type DriveLimits = {
|
|
14
|
+
right: Vector2;
|
|
15
|
+
left: Vector2;
|
|
16
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pth_to_lyt",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"main": "dist/main.js",
|
|
5
|
+
"private": false,
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "tsx watch src/main.ts",
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"serve:production": "node dist/main.js"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@types/node": "^20.10.5",
|
|
13
|
+
"prettier": "^3.7.4",
|
|
14
|
+
"tsx": "^4.7.0",
|
|
15
|
+
"typescript": "^5.3.3"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { parserPTH } from "input/PTH.parser";
|
|
2
|
+
import { readPTH } from "input/PTH.reader";
|
|
3
|
+
import { PTH } from "input/types";
|
|
4
|
+
import { transformLYT } from "output/LYT.transform";
|
|
5
|
+
import { writeLYT } from "output/LYT.writer";
|
|
6
|
+
import { LYTObject } from "output/types";
|
|
7
|
+
|
|
8
|
+
export function convertPTHtoLYT(
|
|
9
|
+
pthFolderPath: string,
|
|
10
|
+
lytFolderPath: string,
|
|
11
|
+
trackPrefix: string,
|
|
12
|
+
lytName: string,
|
|
13
|
+
) {
|
|
14
|
+
const pthPath: string = `${pthFolderPath}/${trackPrefix}.pth`;
|
|
15
|
+
const lytPath: string = `${lytFolderPath}/${trackPrefix}_${lytName}.lyt`;
|
|
16
|
+
|
|
17
|
+
const raw: Buffer<ArrayBuffer> = readPTH(pthPath);
|
|
18
|
+
const data: PTH = parserPTH(raw);
|
|
19
|
+
const objectArray: LYTObject[] = transformLYT(
|
|
20
|
+
data.mainNodes,
|
|
21
|
+
data.numberNodes,
|
|
22
|
+
);
|
|
23
|
+
writeLYT(lytPath, objectArray);
|
|
24
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { PTHNode, LocalHeader, PTH, SystemHeader } from "./types";
|
|
2
|
+
|
|
3
|
+
export function parserPTH(buffer: Buffer<ArrayBuffer>): PTH {
|
|
4
|
+
const systemHeader: SystemHeader = readSystemHeader(buffer);
|
|
5
|
+
const localHeader: LocalHeader = readLocalHeader(buffer);
|
|
6
|
+
const numberNodes: number = readNumberNodes(buffer);
|
|
7
|
+
const mainNodes: PTHNode[] = readMainNodes(buffer, numberNodes);
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
systemHeader,
|
|
11
|
+
localHeader,
|
|
12
|
+
numberNodes,
|
|
13
|
+
mainNodes,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readSystemHeader(buffer: Buffer<ArrayBuffer>): SystemHeader {
|
|
18
|
+
let offset: number = 0;
|
|
19
|
+
const magic: string = buffer.toString("ascii", offset, offset + 6);
|
|
20
|
+
offset += 6;
|
|
21
|
+
|
|
22
|
+
if (magic !== "SRPATH") {
|
|
23
|
+
throw new Error("Invalid File (SRPATH not found)");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const version: number = buffer.readUInt8(offset++);
|
|
27
|
+
const revision: number = buffer.readUInt8(offset++);
|
|
28
|
+
|
|
29
|
+
if (version > 0 || revision > 252) {
|
|
30
|
+
throw new Error("Wrong version");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const flags: number = buffer.readInt32LE(offset);
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
version,
|
|
37
|
+
revision,
|
|
38
|
+
flags,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readLocalHeader(buffer: Buffer<ArrayBuffer>): LocalHeader {
|
|
43
|
+
let offset: number = 12;
|
|
44
|
+
const miniRev: number = buffer.readUInt8(offset++);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
miniRev,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function readNumberNodes(buffer: Buffer<ArrayBuffer>): number {
|
|
52
|
+
let offset: number = 16;
|
|
53
|
+
const numberNodes = buffer.readUInt16LE(offset);
|
|
54
|
+
if (numberNodes <= 0) throw new Error("The number of nodes is too low");
|
|
55
|
+
|
|
56
|
+
return numberNodes;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readNode(offset: number, buffer: Buffer<ArrayBuffer>): PTHNode {
|
|
60
|
+
const node: PTHNode = {
|
|
61
|
+
centre: {
|
|
62
|
+
x: buffer.readInt32LE(offset + 4),
|
|
63
|
+
y: buffer.readInt32LE(offset + 8),
|
|
64
|
+
},
|
|
65
|
+
dir: {
|
|
66
|
+
x: buffer.readFloatLE(offset + 16),
|
|
67
|
+
y: buffer.readFloatLE(offset + 20),
|
|
68
|
+
},
|
|
69
|
+
limits: {
|
|
70
|
+
driveLeft: buffer.readFloatLE(offset + 36),
|
|
71
|
+
driveRight: buffer.readFloatLE(offset + 40),
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
return node;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function readMainNodes(buffer: Buffer<ArrayBuffer>, length: number): PTHNode[] {
|
|
78
|
+
const startOffset: number = 56;
|
|
79
|
+
const nodes: PTHNode[] = [];
|
|
80
|
+
for (let i: number = 0; i < length; i++) {
|
|
81
|
+
const offset: number = startOffset + 44 * i;
|
|
82
|
+
nodes[i] = readNode(offset, buffer);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return nodes;
|
|
86
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type PTH = {
|
|
2
|
+
systemHeader: SystemHeader;
|
|
3
|
+
localHeader: LocalHeader;
|
|
4
|
+
numberNodes: number;
|
|
5
|
+
mainNodes: PTHNode[];
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type SystemHeader = {
|
|
9
|
+
version: number;
|
|
10
|
+
revision: number;
|
|
11
|
+
flags: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type LocalHeader = {
|
|
15
|
+
miniRev: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type PTHNode = {
|
|
19
|
+
centre: {
|
|
20
|
+
x: number;
|
|
21
|
+
y: number;
|
|
22
|
+
};
|
|
23
|
+
dir: {
|
|
24
|
+
x: number;
|
|
25
|
+
y: number;
|
|
26
|
+
};
|
|
27
|
+
limits: {
|
|
28
|
+
driveLeft: number;
|
|
29
|
+
driveRight: number;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { LFS_METER, SCALAR_FACTOR } from "./constants";
|
|
2
|
+
import { PTHNode } from "input/types";
|
|
3
|
+
import { DriveLimits, LYTObject, Vector2 } from "./types";
|
|
4
|
+
|
|
5
|
+
// LFS NOTATION
|
|
6
|
+
// 0 = world y axis direction
|
|
7
|
+
|
|
8
|
+
// LYT NOTATION
|
|
9
|
+
// 128 : heading of zero
|
|
10
|
+
// 192 : heading of 90 degrees
|
|
11
|
+
// 0 : heading of 180 degrees
|
|
12
|
+
// 64 : heading of -90 degrees
|
|
13
|
+
|
|
14
|
+
export function transformLYT(
|
|
15
|
+
nodes: PTHNode[],
|
|
16
|
+
numberNodes: number,
|
|
17
|
+
): LYTObject[] {
|
|
18
|
+
const ZBYTE: number = 240;
|
|
19
|
+
const CHECKPOINT_INDEX: number = 252;
|
|
20
|
+
const FIRST_CHECKPOINT_FLAG: number = 0x01;
|
|
21
|
+
const MAX_NODE: number = 180;
|
|
22
|
+
|
|
23
|
+
const objectArray: LYTObject[] = [];
|
|
24
|
+
const length: number = numberNodes < MAX_NODE ? numberNodes : MAX_NODE;
|
|
25
|
+
const gap: number = numberNodes / length;
|
|
26
|
+
|
|
27
|
+
for (let i: number = 0; i < length; i++) {
|
|
28
|
+
const nodeIndex: number = Math.round(gap * i);
|
|
29
|
+
const node: PTHNode = nodes[nodeIndex];
|
|
30
|
+
|
|
31
|
+
const heading: number = calculateHeading(node.dir.x, node.dir.y);
|
|
32
|
+
const driveLimits: DriveLimits = calculateDriveLimits(node);
|
|
33
|
+
const halfWidth: number = calculateHalfWidth(driveLimits);
|
|
34
|
+
const midPoint: Vector2 = calculateMidPoint(driveLimits);
|
|
35
|
+
|
|
36
|
+
const lytObjectX: number = transformLYTObjectPosition(midPoint.x);
|
|
37
|
+
const lytObjectY: number = transformLYTObjectPosition(midPoint.y);
|
|
38
|
+
|
|
39
|
+
objectArray.push({
|
|
40
|
+
x: lytObjectX,
|
|
41
|
+
y: lytObjectY,
|
|
42
|
+
z: ZBYTE,
|
|
43
|
+
flags: (halfWidth << 2) | FIRST_CHECKPOINT_FLAG,
|
|
44
|
+
index: CHECKPOINT_INDEX,
|
|
45
|
+
heading: heading,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return objectArray;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function transformLYTObjectPosition(value: number): number {
|
|
53
|
+
return Math.round(value / SCALAR_FACTOR);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function calculateHeading(dirX: number, dirY: number): number {
|
|
57
|
+
const angleRadians: number = Math.atan2(dirY, dirX);
|
|
58
|
+
const angleDegrees: number = angleRadians * (180 / Math.PI);
|
|
59
|
+
//"Heading represents 360 degrees in 256 values."
|
|
60
|
+
const heading: number =
|
|
61
|
+
Math.round(((angleDegrees - 90 + 180) * 256) / 360) & 0xff;
|
|
62
|
+
|
|
63
|
+
return heading;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function calculateDriveLimits(node: PTHNode): DriveLimits {
|
|
67
|
+
// "A node is represented by a line perpendicular to its direction."
|
|
68
|
+
// perpendicular = 90 degrees
|
|
69
|
+
// rotate 90 degrees to left for find drive limits direct
|
|
70
|
+
const perpendicularX: number = -node.dir.y;
|
|
71
|
+
const perpendicularY: number = node.dir.x;
|
|
72
|
+
|
|
73
|
+
// driveLeft negative
|
|
74
|
+
// driveRight positive
|
|
75
|
+
const left = {
|
|
76
|
+
x: node.centre.x - perpendicularX * node.limits.driveLeft * LFS_METER,
|
|
77
|
+
y: node.centre.y - perpendicularY * node.limits.driveLeft * LFS_METER,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const right = {
|
|
81
|
+
x: node.centre.x - perpendicularX * node.limits.driveRight * LFS_METER,
|
|
82
|
+
y: node.centre.y - perpendicularY * node.limits.driveRight * LFS_METER,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return { left, right };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function calculateMidPoint(driveLimits: DriveLimits): Vector2 {
|
|
89
|
+
const x: number = (driveLimits.right.x + driveLimits.left.x) / 2;
|
|
90
|
+
const y: number = (driveLimits.right.y + driveLimits.left.y) / 2;
|
|
91
|
+
|
|
92
|
+
return { x, y };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// "half width in metres (1 to 31 ...)."
|
|
96
|
+
export function calculateHalfWidth(
|
|
97
|
+
driveLimits: DriveLimits,
|
|
98
|
+
widthOffset: number = 2,
|
|
99
|
+
): number {
|
|
100
|
+
const distanceX: number = Math.pow(
|
|
101
|
+
driveLimits.right.x - driveLimits.left.x,
|
|
102
|
+
2,
|
|
103
|
+
);
|
|
104
|
+
const distanceY: number = Math.pow(
|
|
105
|
+
driveLimits.right.y - driveLimits.left.y,
|
|
106
|
+
2,
|
|
107
|
+
);
|
|
108
|
+
const distance: number = Math.sqrt(distanceX + distanceY);
|
|
109
|
+
const halfWidth: number = Math.round(
|
|
110
|
+
distance / LFS_METER / 2 + widthOffset,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
return halfWidth > 31 ? 31 : halfWidth;
|
|
114
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import { LYTObject } from "./types";
|
|
3
|
+
|
|
4
|
+
export function writeLYT(path: string, objectArray: LYTObject[]) {
|
|
5
|
+
const bufferHeader: Buffer<ArrayBuffer> = buildBufferHeader(
|
|
6
|
+
objectArray.length,
|
|
7
|
+
);
|
|
8
|
+
const bufferObjects: Buffer<ArrayBuffer> =
|
|
9
|
+
buildBufferLYTObject(objectArray);
|
|
10
|
+
const bufferList: Buffer<ArrayBuffer>[] = [bufferHeader, bufferObjects];
|
|
11
|
+
const buffer: Buffer<ArrayBuffer> = Buffer.concat(bufferList);
|
|
12
|
+
|
|
13
|
+
fs.writeFileSync(path, buffer);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function buildBufferHeader(length: number) {
|
|
17
|
+
const buffer: Buffer<ArrayBuffer> = Buffer.alloc(12);
|
|
18
|
+
|
|
19
|
+
buffer.write("LFSLYT", 0, "ascii");
|
|
20
|
+
buffer.writeUInt8(0, 6);
|
|
21
|
+
buffer.writeUInt8(252, 7);
|
|
22
|
+
buffer.writeUInt16LE(length, 8);
|
|
23
|
+
buffer.writeUInt8(0, 10);
|
|
24
|
+
buffer.writeUInt8(9, 11);
|
|
25
|
+
|
|
26
|
+
return buffer;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function buildBufferLYTObject(objectArray: LYTObject[]) {
|
|
30
|
+
const buffer: Buffer<ArrayBuffer> = Buffer.alloc(8 * objectArray.length);
|
|
31
|
+
let offset: number = 0;
|
|
32
|
+
|
|
33
|
+
for (let i: number = 0; i < objectArray.length; i++) {
|
|
34
|
+
const object: LYTObject = objectArray[i];
|
|
35
|
+
|
|
36
|
+
buffer.writeInt16LE(object.x, offset);
|
|
37
|
+
offset += 2;
|
|
38
|
+
buffer.writeInt16LE(object.y, offset);
|
|
39
|
+
offset += 2;
|
|
40
|
+
buffer.writeUInt8(object.z, offset);
|
|
41
|
+
offset++;
|
|
42
|
+
buffer.writeUInt8(object.flags, offset);
|
|
43
|
+
offset++;
|
|
44
|
+
buffer.writeUInt8(object.index, offset);
|
|
45
|
+
offset++;
|
|
46
|
+
buffer.writeUInt8(object.heading, offset);
|
|
47
|
+
offset++;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return buffer;
|
|
51
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type LYTObject = {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
z: number;
|
|
5
|
+
flags: number;
|
|
6
|
+
index: number;
|
|
7
|
+
heading: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type Vector2 = {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type DriveLimits = {
|
|
16
|
+
right: Vector2;
|
|
17
|
+
left: Vector2;
|
|
18
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"baseUrl": "./src",
|
|
4
|
+
"rootDir": "./src",
|
|
5
|
+
"outDir": "./dist",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"moduleResolution": "Node",
|
|
9
|
+
"target": "es2015",
|
|
10
|
+
"module": "commonjs",
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"skipDefaultLibCheck": true,
|
|
13
|
+
"esModuleInterop": true,
|
|
14
|
+
"noImplicitAny": true
|
|
15
|
+
},
|
|
16
|
+
"ts-node": {
|
|
17
|
+
"esm": true
|
|
18
|
+
},
|
|
19
|
+
"include": ["**/*.ts", "src"],
|
|
20
|
+
"exclude": ["dist", "node_modules"]
|
|
21
|
+
}
|