Improved Misc objects parsing

+Added AngleHelper.normalizeDegVector
+Added support for MiscGroups
+Added an option to skip base Misc objects parsing
*Moved nextMiscId from Scenery to SceneryParser
*Cleane up SceneryParser code
This commit is contained in:
izawartka 2025-07-19 12:22:38 +02:00
parent 15c7d8ba9e
commit c6b6fef724
7 changed files with 108 additions and 27 deletions

View File

@ -1,3 +1,5 @@
import Vector3 from "../model/vector3";
function degToRad(angle) {
return angle * (Math.PI / 180);
}
@ -16,10 +18,19 @@ function normalizeDegAngle(angle) {
}
}
function normalizeDegVector(vector) {
return new Vector3(
normalizeDegAngle(vector.x),
normalizeDegAngle(vector.y),
normalizeDegAngle(vector.z)
)
}
const AngleHelper = {
degToRad,
radToDeg,
normalizeDegAngle,
normalizeDegVector
}
export default AngleHelper;

View File

@ -20,6 +20,7 @@ const Constants = {
attachSignsMaxDistanceX: 0.2,
attachSignsGridSize: 10,
logAttachedSigns: false,
skipBaseMisc: true
},
warnings: {
all: false, // enable all warnings
@ -37,9 +38,9 @@ const Constants = {
invalidSceneryInfoVersion: true,
signUndefinedPrefabName: true,
tracksConnectionTest: true,
tracksConnectionTestSwitchRelated: true,
electrificationNevpNotApplied: false,
electrificationConflict: false,
tracksConnectionTestSwitchRelated: true,
electrificationResolverError: true,
electrificationResolverWarnings: true,
signalElemsUnknownPrefab: true,
@ -50,6 +51,7 @@ const Constants = {
connectTracksFailed: true,
routeInvalidSegment: true,
derailerUnknownDirection: true,
endMiscGroupWithoutStart: true,
},
errors: {
invalidSceneryInfo: true

21
src/model/misc-group.js Normal file
View File

@ -0,0 +1,21 @@
import Vector3 from "./vector3";
export default class MiscGroup {
pos;
rot;
constructor(pos, rot) {
this.pos = pos;
this.rot = rot;
}
static fromText(text) {
const values = text.split(";");
const object = new MiscGroup(
Vector3.fromValuesArray(values, 3), // pos
Vector3.fromValuesArray(values, 6), // rot
);
return object;
}
}

View File

@ -1,3 +1,4 @@
import AngleHelper from "../../helpers/angleHelper";
import SceneryObject from "../scenery-object";
import Vector3 from "../vector3";
@ -6,7 +7,6 @@ export default class Misc extends SceneryObject {
name;
category = "misc";
type = "Misc";
applied = false;
constructor(id, misc_id, prefab_name, pos, rot, name) {
super(id, pos, rot);
@ -17,17 +17,37 @@ export default class Misc extends SceneryObject {
});
}
static fromText(id, text) {
static fromText(id, text, miscGroups = []) {
const values = text.split(";");
const object = new Misc(
id, // id
values[1], // misc_id
values[2], // prefab_name
Vector3.fromValuesArray(values, 3), // pos
Vector3.fromValuesArray(values, 6), // rot
...Misc.applyGroupTransforms(
Vector3.fromValuesArray(values, 3), // pos
Vector3.fromValuesArray(values, 6), // rot
miscGroups
),
values[9] // name
);
return object;
}
static applyGroupTransforms(pos, rot, miscGroups) {
if(!miscGroups) return [ pos, rot ];
for (const group of miscGroups) {
const rotRad = AngleHelper.degToRad(group.rot.y);
const rotRadY = new Vector3(0, rotRad, 0);
pos = pos.rotate(rotRadY).add(group.pos);
rot = rot.add(group.rot);
// TODO: Replace this with some fancy quaternion math maybe
}
rot = AngleHelper.normalizeDegVector(rot);
return [ pos, rot ];
}
}

View File

@ -25,14 +25,21 @@ export default class SignalBox extends Misc {
scenery.addSignalBox(this);
}
static fromText(id, text) {
static isSignalBox(prefabName) {
return prefabName.startsWith('SignalBox');
}
static fromText(id, text, miscGroups = []) {
const values = text.split(";");
const object = new SignalBox(
id, // id
values[1], // misc_id
values[2], // prefab_name
Vector3.fromValuesArray(values, 3), // pos
Vector3.fromValuesArray(values, 6), // rot
...Misc.applyGroupTransforms(
Vector3.fromValuesArray(values, 3), // pos
Vector3.fromValuesArray(values, 6), // rot
miscGroups
),
values[9] // name
);

View File

@ -19,17 +19,18 @@ import Derailer from './track-objects/derailer';
import SpawnPoint from './track-objects/spawn-point';
import { attachSigns } from './attach-signs';
import {connectTracks} from "./connect-tracks";
/*
TODO: add support for WorldRotation and WorldTranslation
*/
import MiscGroup from './misc-group';
export default class SceneryParser {
static nextMiscId = 1;
static fromText(scText) {
SceneryParser.nextMiscId = 1;
const lines = scText.split("\n").map(line => line.trim());
const scenery = new Scenery();
let currentRoute = null;
const currentMiscGroups = [];
lines.forEach((line, index) => {
if(index === 0) {
@ -37,22 +38,30 @@ export default class SceneryParser {
if(sceneryInfo) scenery.addObject(sceneryInfo);
return;
}
if(!line?.length) return;
if (currentRoute !== null) {
const foundRouteEnd = SceneryParser._parseRouteLine(line, currentRoute);
if (foundRouteEnd) {
const isRouteEnd = SceneryParser._parseRouteLine(line, currentRoute);
if (isRouteEnd) {
scenery.addObject(currentRoute);
currentRoute = null;
}
} else {
const object = SceneryParser._parseObject(line);
if(!object) return; // skip null objects
if (object.type === 'Route') {
return;
}
const object = SceneryParser._parseObject(line, currentMiscGroups);
if(!object) return; // skip null objects
switch(object.type) {
case 'Route':
currentRoute = object;
} else {
return; // do not add Route object yet
default:
scenery.addObject(object);
}
break;
}
});
@ -80,7 +89,7 @@ export default class SceneryParser {
return SceneryInfo.fromText(text);
}
static _parseObject(text) {
static _parseObject(text, currentMiscGroups) {
const type = text.split(";", 2)[0];
if (type === 'EndRoute' || type.indexOf("Forest") !== -1 || type.indexOf("Empty") !== -1) {
@ -98,13 +107,22 @@ export default class SceneryParser {
case 'Route':
return Route.fromText(text);
case 'Misc':
return SceneryParser._parseMisc(text);
return SceneryParser._parseMisc(text, currentMiscGroups);
case 'CameraHome':
return CameraHome.fromText(text);
case 'MainCamera':
return MainCamera.fromText(text);
case 'MiscGroup':
const group = MiscGroup.fromText(text);
currentMiscGroups.unshift(group);
return null;
case 'EndMiscGroup':
if(!currentMiscGroups.shift()) {
SceneryParserLog.warn('endMiscGroupWithoutStart', 'Unexpected EndMiscGroup found without a preceding MiscGroup');
}
return null;
case 'Wires':
case 'Fence':
case 'TerrainPoint':
@ -152,13 +170,16 @@ export default class SceneryParser {
}
}
static _parseMisc(text) {
static _parseMisc(text, miscGroups) {
const prefabName = text.split(';', 4)[2];
const id = SceneryParser.nextMiscId++;
if(prefabName.startsWith("SignalBox")) {
return SignalBox.fromText(Scenery.nextMiscId++, text);
if(SignalBox.isSignalBox(prefabName)) {
return SignalBox.fromText(id, text, miscGroups);
} else if(Constants.parser.skipBaseMisc) {
return null;
} else {
return Misc.fromText(Scenery.nextMiscId++, text);
return Misc.fromText(id, text, miscGroups);
}
}

View File

@ -7,7 +7,6 @@ export default class Scenery
spawnPoints = [];
bounds = { minX: Infinity, minZ: Infinity, maxX: -Infinity, maxZ: -Infinity };
electrificationResolved = ElectrificationResolutionStatus.NOT_RESOLVED;
static nextMiscId = 1;
getBounds () {
return { ...this.bounds };